# Bkper Platform (Full)

---
source: /docs/platform.md

# Bkper Platform

Bkper is designed to be extended. Whether you're piping CLI commands in a shell script or building a full platform app with a UI, events, and managed hosting — the same APIs and tools power everything.

## The building spectrum

**Scripts & CLI** — Pipe data through the CLI, write Node.js scripts, or call the REST API directly. No infrastructure needed.

**Google Workspace** — Build automations with Apps Script, extend Google Sheets with custom functions and triggers.

**Platform Apps** — Full applications with managed hosting, authentication, event handling, and deployment on the [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md).

**AI-assisted development** — Prefer CLI local tools when an agent can run terminal commands. Use MCP for conversational connector access. Keep deterministic financial outputs in scripts, tests, and audited artifacts.

## Where to start

    - [Development Setup](https://bkper.com/docs/platform/getting-started/setup.md): Install the CLI, authenticate, and verify your environment.
    - [Quick Wins](https://bkper.com/docs/platform/getting-started/quick-wins.md): The fastest ways to create value with Bkper programmatically.
    - [Your First App](https://bkper.com/docs/platform/apps/first-app.md): Build and deploy a platform app in minutes.
    - [Coding Agents](https://bkper.com/docs/ai/coding-agents.md): Use AI coding agents with Bkper context, CLI tools, and deterministic checks.

## Explore the platform

    - [Scripts & Integrations](https://bkper.com/docs/platform/scripts/cli-pipelines.md): CLI piping, Node.js scripts, and direct API usage.
    - [Apps](https://bkper.com/docs/platform/apps/overview.md): The Bkper Platform — managed hosting, auth, events, and deployment.
    - [Google Workspace](https://bkper.com/docs/platform/google-workspace/apps-script.md): Apps Script development and Sheets integrations.
    - [Tools](https://bkper.com/docs/platform/tools/cli.md): CLI and libraries for building on Bkper.
    - [Examples & Patterns](https://bkper.com/docs/platform/examples.md): Real-world open source projects to learn from.
    - [CLI vs MCP](https://bkper.com/docs/ai/cli-vs-mcp.md): Prefer CLI local tools when terminal access is available; use MCP for conversational connector access.

---
source: /docs/platform/apps/ai.md

# Add Bkper AI to an App

Bkper AI is the preferred inference provider for Bkper Platform apps. It uses the authenticated user's included AI allowance, attributes usage to the app, and does not require the app to store provider credentials.

Use another provider only when Bkper AI lacks a required capability, model, compliance boundary, or customer-mandated provider. External providers require their own authentication, secrets, billing, and privacy review.

This guide covers the preferred current pattern: a non-streaming response with strict structured output. Streaming, tool calls, file inputs, and agent runtimes require additional design.

## Request flow

Keep model calls behind the app's typed `/api/*` contract:

1. The web client calls an app `/api/*` route with a Bkper bearer token. The template's `auth.authenticatedFetch()` handles this.
2. Bkper validates the token, mounts the user and app identity as outbound context, and removes the token before invoking the app Worker.
3. The Worker calls `https://ai.bkper.app/v1/*` without reading, storing, or forwarding the token.
4. Platform outbound injects Bkper authorization and overwrites `bkper-agent-id` and `bkper-ai-source` with the authenticated app identity.

Event handlers use the same Worker-to-Bkper-AI step. Their outbound context comes from the authenticated Bkper event. A normal page request does not establish user outbound context, so start interactive inference from an authenticated `/api/*` route rather than a page handler.

## Call the app API from the client

Use the authenticated fetch provider already configured by the app template:

```ts
interface AnalyzeRequest {
    first: {
        date: string;
        amount: string;
        description: string;
        fromAccount: string | null;
        toAccount: string | null;
    };
    second: {
        date: string;
        amount: string;
        description: string;
        fromAccount: string | null;
        toAccount: string | null;
    };
}

export async function analyzePair(auth: AuthProvider, request: AnalyzeRequest): Promise {
    return auth.authenticatedFetch('/api/v1/analyze', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(request),
    });
}
```

In a full app, define this operation in the server's Zod/OpenAPI schemas and call it through the generated typed client. The important boundary is that the client authenticates the app API request; the Worker never handles that bearer token directly.

## Discover the current default model

The live model catalog is authoritative. It publishes the current default, model IDs, modalities, structured-output support, reasoning levels, context and output limits, and effective usage rates.

```ts
const AI_BASE_URL = 'https://ai.bkper.app/v1';
type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise;

function isRecord(value: unknown): value is Record<string, unknown> {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
}

export async function getStructuredOutputModel(fetcher: Fetcher = fetch): Promise<string> {
    const response = await fetcher(`${AI_BASE_URL}/models`);
    if (!response.ok) {
        throw new Error(`Bkper AI model discovery failed (${response.status}).`);
    }

    const catalog: unknown = await response.json();
    if (
        !isRecord(catalog) ||
        typeof catalog.default_model !== 'string' ||
        !Array.isArray(catalog.data)
    ) {
        throw new Error('Bkper AI returned an invalid model catalog.');
    }

    const defaultModel = catalog.default_model;
    const model = catalog.data.find(item => isRecord(item) && item.id === defaultModel);
    if (
        !isRecord(model) ||
        !isRecord(model.structured_output) ||
        model.structured_output.json_schema !== true ||
        model.structured_output.strict !== true
    ) {
        throw new Error('The default Bkper AI model does not support strict structured output.');
    }
    return defaultModel;
}
```

This example uses `default_model` after validating the capability required by the request. If an app requires another modality, file type, reasoning level, or limit, intentionally select and validate another model from the catalog's `data` array. Apps may cache the catalog briefly rather than fetching it for every inference request.

## Request strict structured output

Keep inference in a server service and make `fetch` injectable for unit tests. This example sends only the transaction facts needed for duplicate evaluation. It omits transaction IDs, Account IDs, unrelated properties, and other Book data.

```ts
const EvaluationJsonSchema = {
    type: 'object',
    properties: {
        duplicate: { type: 'boolean' },
        strength: { type: 'string', enum: ['Strong', 'Possible'] },
        explanation: { type: 'string', maxLength: 180 },
    },
    required: ['duplicate', 'strength', 'explanation'],
    additionalProperties: false,
} as const;

export interface DuplicateEvaluation {
    duplicate: boolean;
    strength: 'Strong' | 'Possible';
    explanation: string;
}

export class BkperAiError extends Error {
    constructor(
        readonly status: number,
        readonly code: string,
        message: string
    ) {
        super(message);
        this.name = 'BkperAiError';
    }
}

export async function evaluateDuplicate(
    candidate: AnalyzeRequest,
    fetcher: Fetcher = fetch
): Promise {
    const model = await getStructuredOutputModel(fetcher);
    const response = await fetcher(`${AI_BASE_URL}/responses`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
            model,
            instructions:
                'Decide whether both records represent the same movement. ' +
                'Return Strong only when the evidence is compelling.',
            input: [
                {
                    role: 'user',
                    content: [
                        {
                            type: 'input_text',
                            text: JSON.stringify(candidate),
                        },
                    ],
                },
            ],
            text: {
                format: {
                    type: 'json_schema',
                    name: 'duplicate_evaluation',
                    schema: EvaluationJsonSchema,
                    strict: true,
                },
            },
            stream: false,
            store: false,
        }),
    });

    const payload: unknown = await response.json();
    if (!response.ok) {
        const error = readAiError(payload);
        throw new BkperAiError(response.status, error.code, error.message);
    }

    const value: unknown = JSON.parse(getOutputText(payload));
    if (!isDuplicateEvaluation(value)) {
        throw new Error('Bkper AI output did not match the required schema.');
    }
    return value;
}
```

Do not add an `Authorization`, `bkper-agent-id`, or `bkper-ai-source` header to the Worker's Bkper AI request. Platform outbound derives those values from the authenticated app request or event and overwrites them before dispatch.

## Validate the response and preserve errors

Strict structured output constrains generation, but the app must still parse and validate the returned value before using it.

```ts
function getOutputText(payload: unknown): string {
    if (!isRecord(payload) || payload.status !== 'completed' || !Array.isArray(payload.output)) {
        throw new Error('Bkper AI did not return a complete response.');
    }

    const texts: string[] = [];
    for (const item of payload.output) {
        if (!isRecord(item) || item.type !== 'message' || !Array.isArray(item.content)) continue;
        for (const part of item.content) {
            if (isRecord(part) && part.type === 'output_text' && typeof part.text === 'string') {
                texts.push(part.text);
            }
        }
    }
    if (texts.length === 0) throw new Error('Bkper AI returned no output text.');
    return texts.join('');
}

function isDuplicateEvaluation(value: unknown): value is DuplicateEvaluation {
    return (
        isRecord(value) &&
        typeof value.duplicate === 'boolean' &&
        (value.strength === 'Strong' || value.strength === 'Possible') &&
        typeof value.explanation === 'string' &&
        value.explanation.length <= 180
    );
}

function readAiError(payload: unknown): { code: string; message: string } {
    if (
        isRecord(payload) &&
        isRecord(payload.error) &&
        typeof payload.error.code === 'string' &&
        typeof payload.error.message === 'string'
    ) {
        return { code: payload.error.code, message: payload.error.message };
    }
    return { code: 'bkper_ai_error', message: 'Bkper AI request failed.' };
}
```

Preserve the upstream HTTP status, error code, and message when mapping a `BkperAiError` into the app's typed error envelope. Bkper AI centralizes actionable messages such as allowance guidance and pricing links. Bot event responses may reuse that message directly when the surface supports it. Interactive apps can use the status and code to provide a tailored experience without duplicating the upstream policy or CTA.

## Test the boundary

Use a mocked `fetch` to protect the integration contract without making live model calls:

```ts
expect(capturedRequest.headers.get('authorization')).toBeNull();
expect(requestBody.store).toBe(false);
expect(requestBody.stream).toBe(false);
expect(requestBody.text).toMatchObject({
    format: { type: 'json_schema', strict: true },
});
```

Also test that the service:

- validates that the catalog's `default_model` supports strict structured output;
- rejects malformed or schema-incompatible output;
- preserves Bkper AI error status, code, and message;
- does not send internal identifiers or unrelated Book data.

## Implementation checklist

Before considering the integration complete:

- [ ] The client calls a typed `/api/*` route through authenticated fetch.
- [ ] Worker code never reads, stores, or forwards the Bkper bearer token.
- [ ] The app discovers models from `GET /v1/models` and intentionally chooses a returned ID.
- [ ] Inference runs in a server service with an injectable `fetch`.
- [ ] The request uses strict structured output, `stream: false`, and `store: false`.
- [ ] Only data required for the task is sent to inference.
- [ ] Returned JSON is parsed and independently validated.
- [ ] Error status, code, and message remain available to the caller.
- [ ] Unit tests cover the request, response, validation, and error boundaries.
- [ ] The app's normal `npm run check` or `bun run check` succeeds.

## Next steps

- [Read the client-agnostic Bkper AI Provider guide](https://bkper.com/docs/ai/bkper-ai-provider.md) for privacy boundaries, the complete supported profile, and advanced features.
- [Inspect the live model catalog](https://ai.bkper.app/v1/models).
- [Browse the generated AI API reference](https://bkper.com/docs/api/ai.md) when exact request or response schema details are needed.
- [Review the Merge Duplicates implementation](https://github.com/bkper/bkper-apps/tree/main/merge-duplicates) for a platform-app example with deterministic candidate filtering, strict structured output, and human-confirmed merges.

---
source: /docs/platform/apps/app-listing.md

# App Listing

All Bkper apps are listed on the Automations Portal at _[app.bkper.com](https://app.bkper.com/) > Automations > Apps_. Each app has its own page with logo, description, and details:

![App listing on the Automations Portal](https://bkper.com/docs/_astro/bkper-app-listing.BgcbAsjE.png)

App listings are populated from the fields you declare in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md). Sync metadata changes with `bkper app sync`. Deploying code is a separate step.

## Listing fields

Make sure your `bkper.yaml` has the following fields populated for a complete listing:

```yaml
id: your-app-id
name: Your App Name
description: A clear description of what your app does

logoUrl: https://your-app.bkper.app/images/logo.svg
logoUrlDark: https://your-app.bkper.app/images/logo-dark.svg

ownerName: Your Name or Organization
ownerWebsite: https://yourwebsite.com

website: https://your-app.bkper.app
```

See [App Configuration](https://bkper.com/docs/platform/apps/configuration.md) for the full `bkper.yaml` reference.

## Default visibility

By default, installation is limited to the users you've declared in `bkper.yaml`:

```yaml
# Specific Bkper usernames
users: alice bob

# Your entire domain
users: *@yourcompany.com
```

Use Bkper usernames for individual access, not email addresses.

Your team can install and use the app, but it doesn't appear in the public Bkper app directory for other users.

## Publishing to all users

To make your app available to all Bkper users, contact us at [support@bkper.com](mailto:support@bkper.com?subject=Publish+Bkper+App). We'll review your app and, once approved, publish it.

### What the review involves

- **Functionality check** — The app works correctly and handles errors gracefully
- **Quality review** — The implementation follows the [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md)
- **Security review** — Event handlers are idempotent and include loop prevention
- **Listing quality** — The app has a clear name, description, logo, and user-facing documentation

### README matters

Your app's `README.md` is displayed to end users on the app listing page. Write it for the people who will install and use your app — not for developers.

**README should explain:**

- What the app does from a user's perspective
- How to use it (step-by-step for non-technical users)
- What features are available
- API access details when the app intentionally exposes `/api/*` routes for users or integrators

**API access details should stay concise:**

- App base URL for production and preview
- OpenAPI spec URL at `/openapi.json`
- One minimal authenticated example, such as a `curl` call with `Authorization: Bearer <token>`

**README should NOT contain:**

- Tech stack or architecture details
- Build commands or development setup
- Project structure or internal file paths
- Long API references, generated schemas, SDK internals, or route-by-route developer docs

Put developer documentation in `AGENTS.md` or internal docs instead. Keep `README.md` focused on the user experience and any integration entry points users need.

### Where published apps appear

Once published, your app appears in:

- **[bkper.com/apps](https://bkper.com/apps)** — The public app directory
- **Automations Portal** — Inside every Bkper book, users can find and install your app

---
source: /docs/platform/apps/architecture.md

# App Architecture

Bkper platform apps use one Worker bundle per app and environment. The same Worker serves the browser client, app-defined `/api/*` routes, and Bkper event ingress at `/events`.

Treat `/api/*` as the reusable surface for app behavior. The bundled web client is one consumer; scripts, external clients, and agents can call the same routes with bearer authentication.

Follow the [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md) when implementing, changing, or reviewing an app.

## Structure

```txt
my-app/
├── client/
│   ├── index.html
│   ├── package.json
│   ├── vite.config.ts
│   └── src/
│       ├── api/
│       ├── auth/
│       ├── components/
│       └── services/
├── server/
│   ├── package.json
│   └── src/
│       ├── api/
│       ├── events/
│       ├── services/
│       └── index.ts
├── scripts/
├── bkper.yaml
├── env.d.ts
├── package.json
├── package-lock.json
└── tsconfig.json
```

The root npm workspace orchestrates development, tests, builds, and deployment. The template keeps browser dependencies in `client/` and Worker dependencies in `server/`. Add a shared package only when both sides actually need one.

## Client

The client uses:

- [Lit](https://lit.dev/) for components and rendering.
- [Web Awesome](https://webawesome.com/) for UI components.
- [`@bkper/web-design`](https://www.npmjs.com/package/@bkper/web-design) for Bkper design tokens.
- [Vite](https://vitejs.dev/) for development and production builds, configured in `client/vite.config.ts`.

Client code has two data paths. Choose based on who owns the behavior:

- **Direct Bkper calls** use `bkper-js` for generic Bkper data needed only by the browser UI.
- **App API calls** use the generated typed client in `client/src/api/` with `auth.authenticatedFetch()` for app-owned behavior, especially when it needs server-only capabilities or more than one caller.

Keep app-owned behavior in one place. Do not implement the same behavior separately in the UI and the app API.

For stateful feature components, co-locate view, controller, and CSS files in one folder under `components/`. Simple presentational components can remain in one file.

### Client authentication

The client authenticates users with [`@bkper/web-auth`](https://www.npmjs.com/package/@bkper/web-auth). OAuth is preconfigured on the platform, so there are no client IDs, redirect URIs, or consent screens to configure.

```ts
import { Bkper } from 'bkper-js';
import { BkperAuth } from '@bkper/web-auth';

const isLocalDev = ['localhost', '127.0.0.1'].includes(window.location.hostname);
const auth = new BkperAuth({
    baseUrl: isLocalDev ? window.location.origin : undefined,
    onLoginSuccess: () => initializeApp(),
    onLoginRequired: () => showLoginButton(),
});
await auth.init();

const bkper = new Bkper({
    oauthTokenProvider: async () => auth.getAccessToken(),
});
```

`@bkper/web-auth` handles login, redirects, and token refresh. The template keeps this behavior behind `client/src/auth/auth-session.ts`.

See the [@bkper/web-auth API Reference](https://bkper.com/docs/api/bkper-web-auth.md) for the full SDK documentation.

## Server Worker

The server runs on [Cloudflare Workers](https://developers.cloudflare.com/workers/) and uses [Hono](https://hono.dev/) with typed OpenAPI routes. It handles:

- app API routes under `/api/*`;
- Bkper event ingress under `/events`;
- platform services such as KV and secrets through `c.env`;
- static client assets through the `ASSETS` binding.

The Worker entry point composes those concerns while routes delegate business behavior to services:

```ts
import { OpenAPIHono } from '@hono/zod-openapi';
import { registerApiRoutes } from './api/routes.js';
import { registerEventRoutes } from './events/routes.js';
import { appContextMiddleware, type AppEnv } from './app-context.js';

const app = new OpenAPIHono();

app.use('/api/*', appContextMiddleware());
app.use('/events', appContextMiddleware());
registerApiRoutes(app);
registerEventRoutes(app);

app.get('*', c => c.env.ASSETS.fetch(c.req.raw));

export default app;
```

## App API contract

The default template publishes versioned routes under `/api/v1/*` and exposes their OpenAPI contract at `/openapi.json`.

| Concern                      | Location                              |
| ---------------------------- | ------------------------------------- |
| OpenAPI metadata             | `server/src/api/openapi.ts`           |
| Request and response schemas | `server/src/api/schemas.ts`           |
| Thin route handlers          | `server/src/api/routes.ts`            |
| Business behavior            | `server/src/services/`                |
| Generated client types       | `client/src/api/generated/types.d.ts` |
| Typed client wrapper         | `client/src/api/app-api.ts`           |
| Contract snapshot            | `server/test/api/openapi.snapshot.json` |

When changing the API:

1. Update schemas, services, routes, and focused unit tests.
2. Run `npm run api` to regenerate client types.
3. Review the OpenAPI snapshot when the public contract changes.
4. Run `npm run check` before release.

Keep existing `/api/v1/*` contracts backward compatible. Additive fields and routes can remain in `v1`; breaking changes belong in a new namespace such as `/api/v2/*`.

### Reuse Bkper API types

When an app API returns payloads from the Bkper REST API, reference the canonical types from `@bkper/bkper-api-types` instead of recreating their fields in the app. The template's balances endpoint demonstrates this with `bkper.Book`:

```ts
export const BookSchema = z
    .custom<bkper.Book>(value => value !== undefined)
    .openapi('Book', {
        type: 'object',
        additionalProperties: true,
        'x-typescript-type': 'bkper.Book',
    });
```

The template's API generator recognizes `x-typescript-type`, imports `@bkper/bkper-api-types`, and emits the canonical reference in `client/src/api/generated/types.d.ts`:

```ts
Book: bkper.Book;
```

Both the server and client packages include `@bkper/bkper-api-types` for local typechecking. Run `npm run api` after adding or changing these schemas.

This bridge provides compile-time types but does not validate payload fields at runtime. Use it directly for trusted Bkper-owned responses. Request bodies, especially those used to create or modify Book resources, still require concrete Zod validation.

### URLs

```txt
Production API: https://{appId}.bkper.app/api/*
Preview API:    https://{appId}-preview.bkper.app/api/*
Local API:      http://localhost:8787/api/*

Production spec: https://{appId}.bkper.app/openapi.json
Preview spec:    https://{appId}-preview.bkper.app/openapi.json
Local spec:      http://localhost:8787/openapi.json
```

Example script call:

```bash
TOKEN="$(bkper auth token)"

curl \
  -H "Authorization: Bearer ${TOKEN}" \
  "https://my-app.bkper.app/api/v1/ping"
```

Replace `my-app` with the app id from `bkper.yaml`.

### Server API authentication

Deployed `/api/*` routes require a Bkper OAuth bearer token. The template client uses `authenticatedFetch()` so token attachment and refresh stay inside `@bkper/web-auth`:

```ts
const response = await auth.authenticatedFetch('/api/v1/ping');
```

Dispatch validates the incoming bearer token and strips the `Authorization` header before the Worker runs. Server code should not read or forward the token.

When a route calls Bkper, create the SDK without a token provider:

```ts
import { Bkper } from 'bkper-js';

const bkper = new Bkper();
const books = await bkper.getBooks();
```

Platform outbound authentication injects the validated user's OAuth token on Bkper API requests.

### Authorize app operations

Authentication identifies the Bkper user, but each app must authorize sensitive data and actions server-side. Client-side checks are not an authorization boundary.

See [App Security](https://bkper.com/docs/platform/apps/security.md) for domain restrictions, Book permissions, and app installation checks.

## Event handlers

Platform event deliveries reach `/events` on the same Worker. Event adapters live in `server/src/events/`, while reusable business behavior belongs in `server/src/services/`.

Event code uses server-side `new Bkper()` and must not read `bkper-oauth-token`, `bkper-agent-id`, or `Authorization` headers. Dispatch and platform outbound authentication handle the event token and app agent identity.

See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for routing, responses, loop prevention, and event types. Self-hosted handlers process event authentication directly because the platform outbound layer is not involved.

## App shapes

The platform supports different shapes:

- **Full app** — Client UI, `/api/*` backend behavior, and `/events` automation in one Worker. This is the default template.
- **Event-only app** — Keep `server/` and omit `deployment.client`.
- **UI-only app** — Keep a minimal Worker for static assets when behavior is truly browser-only. Add `/api/*` when scripts, integrations, or agents should reuse that behavior.

---
source: /docs/platform/apps/configuration.md

# App Configuration

The `bkper.yaml` file is the single configuration file for your Bkper app. It defines the app's identity, access control, menu integration, event handling, and deployment settings.

It lives in the root of your project. Use `bkper app sync` to push metadata changes to Bkper, and use `bkper app deploy` to upload built code to the platform.

## Minimal example

```yaml
id: my-app
name: My App
description: A Bkper app that does something useful
developers: myuser
```

## Starter example

From the [app template](https://github.com/bkper/bkper-app-template):

```yaml
id: my-app
name: My App
description: A Bkper app that does something useful

logoUrl: https://my-app.bkper.app/images/logo-light.svg
logoUrlDark: https://my-app.bkper.app/images/logo-dark.svg

website: https://my-app.bkper.app
ownerName: Bkper
ownerLogoUrl: https://avatars.githubusercontent.com/u/11943086?v=4
ownerWebsite: https://bkper.com

developers: someuser *@yoursite.com
users: someuser *@yoursite.com

menuUrl: https://my-app.bkper.app?bookId=${book.id}
menuUrlDev: https://my-app-preview.bkper.app?bookId=${book.id}
menuOpenMode: SIDEBAR

webhookUrl: https://my-app.bkper.app/events
webhookUrlDev: https://my-app-preview.bkper.app/events
apiVersion: v5
events:
    - TRANSACTION_CHECKED

deployment:
    server: server/src/index.ts
    client: client
    services:
        - KV
    secrets: []
    compatibility_date: '2026-01-28'
```

### App identity

| Field         | Description                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| `id`          | Permanent app identifier. Lowercase letters, numbers, and hyphens only. Cannot be changed after creation. |
| `name`        | Display name shown in the Bkper UI.                                                                       |
| `description` | Brief description of what the app does.                                                                   |

### Branding

| Field         | Description                                |
| ------------- | ------------------------------------------ |
| `logoUrl`     | App logo for light mode (SVG recommended). |
| `logoUrlDark` | App logo for dark mode.                    |
| `website`     | App website or documentation URL.          |

### Ownership

| Field          | Description                                                  |
| -------------- | ------------------------------------------------------------ |
| `ownerName`    | Developer or company name.                                   |
| `ownerLogoUrl` | Owner's logo/avatar URL.                                     |
| `ownerWebsite` | Owner's website.                                             |
| `repoUrl`      | Source code repository URL.                                  |
| `repoPrivate`  | Whether the repository is private.                           |
| `deprecated`   | Hides from app listings; existing installs continue working. |

### Access control

| Field        | Description                                                                                                                                      |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `developers` | Who can update the app and deploy new versions. Accepts comma- or space-separated Bkper usernames and domain wildcards such as `*@yoursite.com`. |
| `users`      | Who can install and use the app. Uses the same format as `developers`; leave empty for public apps.                                              |

### Menu integration

| Field          | Description                                                                 |
| -------------- | --------------------------------------------------------------------------- |
| `menuUrl`      | Production menu URL. Supports [variable substitution](#menu-url-variables). |
| `menuUrlDev`   | Development menu URL, typically a preview or local app URL.                 |
| `menuText`     | Custom menu text (defaults to app name).                                    |
| `menuOpenMode` | How the app menu opens: `SIDEBAR` (default), `EXPANDED`, or `NEW_TAB`.      |

`SIDEBAR` and `EXPANDED` Apps can receive live context updates while their iframe stays loaded. `NEW_TAB` Apps receive context only in the URL used to open the tab.

See [Context Menu](https://bkper.com/docs/platform/apps/context-menu.md#live-context-updates) for menu URL configuration and live context updates.

### Menu URL variables

The following variables can be used in `menuUrl` and `menuUrlDev`:

| Variable                    | Description                              |
| --------------------------- | ---------------------------------------- |
| `${book.id}`                | Current book ID                          |
| `${book.properties.xxx}`    | Book property value                      |
| `${account.id}`             | Selected account ID                      |
| `${account.name}`           | Selected account name                    |
| `${account.properties.xxx}` | Account property value                   |
| `${group.id}`               | Selected group ID                        |
| `${group.name}`             | Selected group name                      |
| `${group.properties.xxx}`   | Group property value                     |
| `${transactions.ids}`       | Comma-separated selected transaction IDs |
| `${transactions.query}`     | Current search query                     |

### Event handling

| Field           | Description                                                                         |
| --------------- | ----------------------------------------------------------------------------------- |
| `webhookUrl`    | Production webhook URL for receiving events.                                        |
| `webhookUrlDev` | Development webhook URL (auto-updated by `bkper app dev`).                          |
| `apiVersion`    | API version for event payloads (currently `v5`).                                    |
| `events`        | List of [event types](https://bkper.com/docs/platform/apps/event-handlers.md#event-types) to subscribe to. |

See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for details on handling events.

### File patterns

| Field          | Description                                                                                                            |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `filePatterns` | List of glob patterns (e.g., `*.ofx`, `*.csv`). When a matching file is uploaded, a `FILE_CREATED` event is triggered. |

### Properties schema

The `propertiesSchema` field defines autocomplete suggestions for custom properties in the Bkper UI, helping users discover the correct property keys and values for your app.

Suggested keys must follow the same custom property rules as user-entered keys, including the 30-character maximum after normalization.

```yaml
propertiesSchema:
    book:
        keys:
            - my_app_enabled
        values:
            - 'true'
            - 'false'
    group:
        keys:
            - my_app_category
    account:
        keys:
            - my_app_sync_id
    transaction:
        keys:
            - my_app_reference
```

### Deployment

For apps deployed to the [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md):

| Field                           | Description                                                                                                            |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `deployment.server`             | TypeScript entry point for the single server Worker. It serves `/api/*`, `/events`, and static assets.                 |
| `deployment.client`             | Optional Vite/static client root. Built assets are deployed with the same Worker.                                      |
| `deployment.services`           | Platform services to provision. Currently: `KV` (key-value storage).                                                   |
| `deployment.secrets`            | Secret names used by the app. Managed via `bkper app secrets`.                                                         |
| `deployment.compatibility_date` | [Cloudflare Workers compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/). |

See [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) for the full deployment workflow.

---
source: /docs/platform/apps/context-menu.md

# Context Menu

Apps can add context menu items on the Transactions page **More** menu in your Books. This lets you open dynamically built URLs with reference to the current Book's context — the active query, selected account, date range, and more.

Embedded interfaces should follow the [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md) for visual consistency, startup behavior, and Book-context verification.

## How it works

Once you install an App with a menu configuration, a new menu item appears in your Book:

![Custom menu item in the More menu](https://bkper.com/docs/_astro/bkper-report-menu.eu_pyhWe.png)

When clicked, a popup opens carrying the particular context of that book at that moment:

![App menu popup with book context](https://bkper.com/docs/_astro/bkper-app-menu-popup.BQ95Y-ki.png)

## Configuration

Configure the menu URL in your [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md):

```yaml
menuUrl: https://my-app.bkper.app?bookId=${book.id}&query=${transactions.query}
```

When the user clicks the menu item, the URL expressions `${xxxx}` are replaced with contextual information from the Book:

```
https://my-app.bkper.app?bookId=abc123&query=account:Sales
```

Where `abc123` is the current Book id and `account:Sales` is the current query being executed.

### Development URL

Use `menuUrlDev` to keep developer testing separate from production. The app template points it to the preview deployment:

```yaml
menuUrl: https://my-app.bkper.app?bookId=${book.id}&query=${transactions.query}
menuUrlDev: https://my-app-preview.bkper.app?bookId=${book.id}&query=${transactions.query}
```

During local development, you can instead point it to the local Worker URL at `http://localhost:8787`. The development URL is used when an app developer clicks the menu item.

### Menu open mode

Control how the menu opens with `menuOpenMode`:

```yaml
menuOpenMode: SIDEBAR
```

| Mode       | Behavior                                                              |
| ---------- | --------------------------------------------------------------------- |
| `SIDEBAR`  | Opens in a narrow side panel (default).                               |
| `EXPANDED` | Opens in a wider panel with more room for complex UIs.                |
| `NEW_TAB`  | Opens the menu URL in a new browser tab instead of an embedded panel. |

### Live context updates

Bkper keeps embedded Apps informed of context changes without reloading the iframe, allowing them to preserve their current state. For Apps opened in `SIDEBAR` or `EXPANDED`, Bkper communicates those changes by sending the updated App URL to the iframe when its origin remains the same:

```js
{
    type: 'bkper:app-url-changed',
    url: 'https://my-app.bkper.app?bookId=abc123&query=account:Sales',
}
```

Listen for the message in the App:

```js
const BKPER_ORIGIN = 'https://bkper.app';

window.addEventListener('message', event => {
    // Verify that the trusted Bkper parent sent the message.
    if (event.source !== window.parent || event.origin !== BKPER_ORIGIN) return;

    // Verify that this is a valid App URL update.
    const message = event.data;
    if (message?.type !== 'bkper:app-url-changed' || typeof message.url !== 'string') return;

    // Parse the updated URL, ignoring malformed URL strings.
    let nextUrl;
    try {
        nextUrl = new URL(message.url);

        // Accept only URLs belonging to this App.
        if (nextUrl.origin !== window.location.origin) return;
    } catch {
        return;
    }

    // Keep the iframe URL in sync without reloading it.
    window.history.replaceState(window.history.state, '', nextUrl);

    // Apply the validated context update.
    handleAppUrlChange(nextUrl);
});
```

`handleAppUrlChange` is App logic. The App can update internal state, notify components, refresh data, change its UI, or ignore the message. Bkper only communicates the new URL; it does not reload the iframe or apply the context inside the App.

Apps opened with `NEW_TAB` do not receive this message. Their context is set only by the URL used to open the tab.

### Available expressions

The menu URL supports these dynamic expressions:

| Expression              | Description               |
| ----------------------- | ------------------------- |
| `${book.id}`            | The current Book ID       |
| `${transactions.query}` | The current query string  |
| `${account.id}`         | The selected account ID   |
| `${account.name}`       | The selected account name |
| `${group.id}`           | The selected group ID     |
| `${group.name}`         | The selected group name   |

For the full list of accepted expressions, see the [Menu URL variables](https://bkper.com/docs/platform/apps/configuration.md#menu-url-variables) reference.

---
source: /docs/platform/apps/deploying.md

# Building & Deploying

## The deployment workflow

Sync and deployment require an attached, clean, committed Git branch whose current commit is stored remotely. Bkper-managed private Git is recommended. Apps without Git receive actionable initialization and managed-sync instructions from the CLI; external repositories must configure and push the current branch to its intended upstream.

Run the template's deterministic checks before releasing:

```bash
npm run check
```

1. **Build** — Compile your code

    ```bash
    npm run build
    ```

    This runs two build steps:
    - Client (Vite) to static assets in `dist/client/`
    - Server Worker bundle (esbuild) to `dist/server/`

    Build output includes size reporting so you can monitor bundle sizes.

2. **Sync** — Update app metadata and managed source

    ```bash
    bkper app sync
    ```

    Verifies stored source, then syncs your `bkper.yaml` configuration to Bkper — name, description, menu URLs, webhook URLs, access control, and branding. For managed source, it safely pushes the current clean, committed branch. For external source, it fetches the configured upstream and verifies that it contains the current commit. Sync does not build or deploy the app.

3. **Deploy** — Upload the local build to the platform

    ```bash
    bkper app deploy
    ```

    Deploy verifies stored source again. Managed source is pushed and Platform-verified; external source is checked against the configured upstream by the CLI. Deploy then uploads your existing pre-built code from `dist/` to the Bkper Platform. The command does not run a build, and the Platform does not prove that `dist/` was produced from the verified commit. Your app is live at `https://{appId}.bkper.app`.

The app template combines all three after source changes are committed:

```bash
npm run deploy
```

Use `npm run deploy:preview` for the preview environment.

> **Caution: Source is not deployment**
> An ordinary `git push` stores source only and never deploys. `bkper app sync` also does not deploy. Run `bkper app deploy` explicitly when the local build is ready to release.
See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for managed-source setup, cloning, access, and external Git workflows.

### Production

The default deployment target. Your app runs at `https://{appId}.bkper.app`.

```bash
bkper app deploy
```

Production serves:

```txt
Client:       https://{appId}.bkper.app
API routes:   https://{appId}.bkper.app/api/*
OpenAPI spec: https://{appId}.bkper.app/openapi.json
Events:       https://{appId}.bkper.app/events
```

### Preview

Deploy to a separate preview environment for testing before production:

```bash
bkper app deploy --preview
```

Preview URLs use a dash suffix: `https://{appId}-preview.bkper.app`. For example, an app with `id: my-app` deploys to `https://my-app-preview.bkper.app`.

Preview serves:

```txt
Client:       https://{appId}-preview.bkper.app
API routes:   https://{appId}-preview.bkper.app/api/*
OpenAPI spec: https://{appId}-preview.bkper.app/openapi.json
Events:       https://{appId}-preview.bkper.app/events
```

Preview has independent secrets and KV storage from production.

There is one app deployment per environment. `/events` is handled by the same Worker as the client assets and `/api/*` routes.

## Secrets management

Secrets are environment variables stored securely on the platform. Declare them in `bkper.yaml`:

```yaml
deployment:
    secrets:
        - EXTERNAL_SERVICE_TOKEN
```

### Setting secrets

```bash
# Set for production
bkper app secrets put EXTERNAL_SERVICE_TOKEN

# Set for preview
bkper app secrets put EXTERNAL_SERVICE_TOKEN --preview
```

You'll be prompted to enter the value.

### Listing and deleting

```bash
# List all secrets
bkper app secrets list

# Delete a secret
bkper app secrets delete EXTERNAL_SERVICE_TOKEN
```

### Accessing in code

Secrets are available as `c.env.SECRET_NAME` in your Hono handlers:

```ts
app.get('/api/data', async c => {
    const token = c.env.EXTERNAL_SERVICE_TOKEN;
    // use token
});
```

During local development, use the `.dev.vars` file instead. See [Development Experience](https://bkper.com/docs/platform/apps/development.md#local-secrets).

### KV storage

Declare KV in `bkper.yaml`:

```yaml
deployment:
    services:
        - KV
```

The platform provisions a KV namespace for your app. Access it via `c.env.KV`:

```ts
await c.env.KV.put('key', 'value', { expirationTtl: 3600 });
const value = await c.env.KV.get('key');
```

KV storage is separate between production and preview environments.

## Deployment status

Check the current state of your deployment:

```bash
bkper app status
```

## Installing on books

After deploying, install the app on specific books to activate it:

```bash
# Install on a book
bkper app install <appId> -b <bookId>

# Uninstall from a book
bkper app uninstall <appId> -b <bookId>
```

Once installed, the app's [event handlers](https://bkper.com/docs/platform/apps/event-handlers.md) receive events from that book at `/events`, and the app's [context menu](https://bkper.com/docs/platform/apps/context-menu.md) appears in the book's UI.

## Next steps

- [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) — Share private source without coupling Git pushes to deployment
- [Development Experience](https://bkper.com/docs/platform/apps/development.md) — Run the app and event delivery locally
- [App Listing](https://bkper.com/docs/platform/apps/app-listing.md) — Prepare the app for installation

---
source: /docs/platform/apps/development.md

# Development Experience

Local development uses two composable processes — the worker runtime and the client dev server — that run concurrently.

## What runs

```bash
npm run dev
```

The project template runs both processes via `concurrently`:

1. **`vite dev`** — Client dev server with HMR. Changes to Lit components reflect instantly in the browser. Configured in `client/vite.config.ts`.
2. **`bkper app dev`** — The worker runtime:
    - **Miniflare** — Simulates the single Cloudflare Worker locally.
    - **Cloudflare tunnel** — Exposes `/events` via a public URL so Bkper can route webhook events to your machine.
    - **File watching** — Server changes trigger automatic rebuilds via esbuild.

You can also run them independently: `npm run dev:client` for just the UI, or `npm run dev:server` for the local Worker.

## URLs

| Endpoint                               | URL                                         |
| -------------------------------------- | ------------------------------------------- |
| Client (Vite dev server)               | `http://localhost:5173`                     |
| Server Worker (Miniflare)              | `http://localhost:8787`                     |
| App API routes                         | `http://localhost:8787/api/*`               |
| App OpenAPI spec                       | `http://localhost:5173/openapi.json`        |
| Events (via tunnel to the same Worker) | `https://<random>.trycloudflare.com/events` |

The Vite dev server proxies `/api` and `/openapi.json` requests to `http://localhost:8787` through `client/vite.config.ts`, so the client and OpenAPI spec share the same local origin just as they do in production. The spec also remains available directly from the Worker at `http://localhost:8787/openapi.json`. The tunnel URL is automatically registered as `webhookUrlDev`, so development-mode events are routed to your local machine.

## Configuration flags

There is one local Worker. Override its port when needed:

```bash
bkper app dev --sp 8787
```

## Client configuration

The client dev server is configured in `client/vite.config.ts`. This standard Vite configuration registers local auth middleware and proxies `/api` and `/openapi.json` requests to the Worker.

### Local development authentication

During local development, the Vite dev server runs `createBkperAuthMiddleware()` from `bkper/dev`. It serves the local `/auth/refresh` endpoint used by `@bkper/web-auth`, obtaining OAuth tokens from your CLI credentials.

The separate Vite proxy configuration forwards `/api` and `/openapi.json` requests to the Miniflare Worker.

Before starting development, run:

```bash
bkper auth login   # one-time setup
```

Then `npm run dev` handles local authentication. Direct `bkper-js` calls use `auth.getAccessToken()`, while the typed app API client uses `auth.authenticatedFetch()` to attach and refresh bearer authentication.

Local outbound uses your CLI credentials when the app server or event handler calls Bkper.

If you see authentication errors in the browser, verify you're logged in:

```bash
bkper auth token   # should print a token
```

This is the canonical pattern for local development. Do not manually pass tokens or implement custom auth flows.

## Local secrets

Environment variables for local development live in a `.dev.vars` file at the project root:

```bash
# .dev.vars (gitignored)
EXTERNAL_SERVICE_TOKEN=your-token-here
```

Copy from the provided template:

```bash
cp .dev.vars.example .dev.vars
```

These variables are available as `c.env.SECRET_NAME` in your Hono handlers during development.

## KV storage

KV data persists locally in the `.mf/kv/` directory during development. This means your data survives restarts — useful for testing caching and state patterns.

```ts
// Read
const value = await c.env.KV.get('my-key');

// Write with TTL
await c.env.KV.put('my-key', 'value', { expirationTtl: 3600 });
```

See the [Cloudflare KV documentation](https://developers.cloudflare.com/kv/) for more usage patterns.

## Type generation

The `env.d.ts` file provides TypeScript types for the Worker environment — KV bindings, secrets, and other platform services. It's auto-generated based on your `bkper.yaml` configuration and checked into version control.

Rebuild it after changing services or secrets in `bkper.yaml`:

```bash
bkper app build
```

## The development loop

1. Run `npm run dev`.
2. Edit client code and use Vite HMR.
3. Edit server code and let the Worker reload.
4. Trigger events in Bkper and inspect handler responses in the activity stream.
5. Run `npm run check` before considering the change complete.

## Debugging

- **Server errors** — Check the terminal output from `bkper app dev`. Worker runtime errors appear here.
- **Event handler errors** — Check the Bkper activity stream. Click on an event handler response to see the result or error, and replay failed events.
- **Client errors** — Use browser DevTools. The Vite dev server provides source maps.

---
source: /docs/platform/apps/event-handlers.md

# Bkper Webhooks and Event Handlers

Event handlers are the code that reacts to events in your Bkper Books. When a transaction is checked, an account is created, or any other event occurs, your handler receives it and can take action — calculate taxes, sync data between books, post to external services, and more.

![Bkper Event Handler](https://bkper.com/images/bots/bkper-tax-bot/bkper-tax-bot.gif)

## How it works

1. You declare which events your app handles in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md)
2. Bkper sends an HTTP POST to your webhook URL when those events fire
3. Your handler processes the event and returns a response

On the [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md), events are routed to `/events` on your app's single Worker — including local development via tunnels. For [self-hosted](https://bkper.com/docs/platform/apps/self-hosted.md) setups, you configure the webhook URL directly.

## Agent identity

Event handlers **run on behalf of the user who installed the app**. Their transactions and activities are identified in the UI by the app's logo and name:

![Event handler agents identified in the activity stream](https://bkper.com/docs/_astro/bkper-bot-agents.CtsWIZEd.png)

## Responses

Handler responses are recorded in the activity that triggered the event. You can view and replay them by clicking the response at the bottom of the activity:

![Event handler responses in the activity stream](https://bkper.com/docs/_astro/bkper-bot-responses.UQXhqdai.png)

### Response format

Your handler must return a response in this format:

```ts
{ result?: string | string[] | boolean; error?: string; warning?: string }
```

- The `result` is recorded as the handler response in the book activity
- If you return `{ result: false }`, the response is suppressed and not recorded
- Errors like `{ error: "This is an error" }` show up as error responses

To show the full error stack trace for debugging:

```ts
try {
    // handler logic
} catch (err) {
    return { error: err instanceof Error ? err.message : String(err) };
}
```

### HTML in responses

If you return an **HTML snippet** (e.g., a link) in the result, it will be rendered in the response popup.

## Development mode

Event handlers run in _Development Mode_ when executed by the **developer or owner** of the App.

In development mode, both successful results and errors are shown as responses:

![Event handler error in development mode](https://bkper.com/docs/_astro/bkper-bot-error.4eq2AKEM.png)

You can click a response to **replay** failed executions — useful for debugging without recreating the triggering event.

To find transactions with bot errors in a book, run the query:

```
error:true
```

## Preventing loops

When your event handler creates or modifies transactions, those changes fire new events. To prevent infinite loops, check the `event.agent.id` field:

```ts
function handleEvent(event: bkper.Event) {
    // Skip events triggered by this app
    if (event.agent?.id === 'your-app-id') {
        return { result: false };
    }

    // Process the event
    // ...
}
```

This pattern is essential for any handler that writes back to the same book.

## Authentication

Platform-hosted event handlers use the same server-side Bkper API pattern as `/api/*` routes:

```ts
const bkper = new Bkper();
const book = new Book(event.book, bkper.getConfig());
```

Dispatch consumes the event delivery token, strips platform headers before your Worker runs, and platform outbound auth injects the OAuth token and app agent identity on Bkper API calls.

Do not read `bkper-oauth-token`, `bkper-agent-id`, or `Authorization` headers in platform app code.

> **Note**
> During local development, events are routed through the Cloudflare tunnel started by `bkper app dev`. Local outbound uses your CLI credentials when the handler calls Bkper.
For [self-hosted](https://bkper.com/docs/platform/apps/self-hosted.md) setups, the event auth headers are sent to both `webhookUrl` and `webhookUrlDev` and must be handled directly by your infrastructure.

## Event routing pattern

On the Bkper Platform, your server Worker uses [Hono](https://hono.dev) to receive webhook calls at `/events`. A typical pattern routes events by type:

```ts
import { Bkper, Book } from 'bkper-js';

app.post('/events', async c => {
    const event: bkper.Event = await c.req.json();

    if (!event.book) {
        return c.json({ error: 'Missing book in event payload' }, 400);
    }

    const bkper = new Bkper();
    const book = new Book(event.book, bkper.getConfig());

    switch (event.type) {
        case 'TRANSACTION_CHECKED':
            return c.json(await handleTransactionChecked(book, event));
        default:
            return c.json({ result: false });
    }
});
```

## The Event object

The event payload has the following structure:

```ts
{
    /** The id of the Book associated to the Event */
    bookId?: string;

    /** The Book object associated with the Event */
    book?: {
        agentId?: string;
        collection?: Collection;
        createdAt?: string;
        datePattern?: string;
        decimalSeparator?: "DOT" | "COMMA";
        fractionDigits?: number;
        id?: string;
        lastUpdateMs?: string;
        lockDate?: string;
        name?: string;
        ownerName?: string;
        pageSize?: number;
        period?: "MONTH" | "QUARTER" | "YEAR";
        periodStartMonth?: "JANUARY" | "FEBRUARY" | "MARCH" | "APRIL"
            | "MAY" | "JUNE" | "JULY" | "AUGUST" | "SEPTEMBER"
            | "OCTOBER" | "NOVEMBER" | "DECEMBER";
        permission?: "OWNER" | "EDITOR" | "POSTER" | "RECORDER"
            | "VIEWER" | "NONE";
        properties?: { [name: string]: string };
        timeZone?: string;
        timeZoneOffset?: number;
    };

    /** The user in charge of the Event */
    user?: {
        avatarUrl?: string;
        name?: string;
        username?: string;
    };

    /** The Event agent, such as the App, Bot or Bank institution */
    agent?: {
        id?: string;
        logo?: string;
        name?: string;
    };

    /** The creation timestamp, in milliseconds */
    createdAt?: string;

    /** The event data */
    data?: {
        /** The object payload. Depends on the event type. */
        object?: any;
        /** The object previous attributes when updated */
        previousAttributes?: { [name: string]: string };
    };

    /** The unique id that identifies the Event */
    id?: string;

    /** The resource associated to the Event */
    resource?: string;

    /** The type of the Event */
    type?: EventType;
}
```

The event payload is the same structure exposed by the [REST API](https://bkper.com/docs/platform/scripts/rest-api.md). If you use TypeScript, add the [`@bkper/bkper-api-types`](https://www.npmjs.com/package/@bkper/bkper-api-types) package to your project for full type definitions.

For update events, `data.previousAttributes` contains the fields that changed and their previous values — useful for computing diffs or reacting only to specific field changes.

## Event types

Declare which events your app handles in `bkper.yaml`:

```yaml
events:
    - TRANSACTION_CHECKED
    - TRANSACTION_POSTED
    - ACCOUNT_CREATED
```

The complete API set of event types is listed below. `COMMENT_CREATED` and `COMMENT_DELETED` remain in the API for compatibility with historical Events; Comments are not available in the current Bkper PWA.

| Event | Description |
| --- | --- |
| `FILE_CREATED` | A file was attached to the book. |
| `FILE_UPDATED` | An attached file was updated. |
| `TRANSACTION_CREATED` | A draft transaction was created. |
| `TRANSACTION_UPDATED` | A transaction was updated. |
| `TRANSACTION_DELETED` | A transaction was deleted. |
| `TRANSACTION_POSTED` | A draft transaction was posted and now affects balances. |
| `TRANSACTION_CHECKED` | A posted transaction was checked (reviewed and locked). |
| `TRANSACTION_UNCHECKED` | A checked transaction was unchecked and becomes editable again. |
| `TRANSACTION_RESTORED` | A deleted transaction was restored. |
| `ACCOUNT_CREATED` | An account was created. |
| `ACCOUNT_UPDATED` | An account was updated. |
| `ACCOUNT_DELETED` | An account was deleted. |
| `QUERY_CREATED` | A saved query was created. |
| `QUERY_UPDATED` | A saved query was updated. |
| `QUERY_DELETED` | A saved query was deleted. |
| `GROUP_CREATED` | A group was created. |
| `GROUP_UPDATED` | A group was updated. |
| `GROUP_DELETED` | A group was deleted. |
| `COMMENT_CREATED` | A comment was added. |
| `COMMENT_DELETED` | A comment was deleted. |
| `COLLABORATOR_ADDED` | A collaborator was added to the book. |
| `COLLABORATOR_UPDATED` | A collaborator's permissions were updated. |
| `COLLABORATOR_REMOVED` | A collaborator was removed from the book. |
| `INTEGRATION_CREATED` | An integration was created in the book. |
| `INTEGRATION_UPDATED` | An integration was updated. |
| `INTEGRATION_DELETED` | An integration was deleted. |
| `BOOK_CREATED` | A book was created. |
| `BOOK_AUDITED` | A balances audit completed for the book. |
| `BOOK_UPDATED` | Book settings were updated. |
| `BOOK_DELETED` | The book was deleted. |

---
source: /docs/platform/apps/first-app.md

# Your First App

This tutorial walks you through building and deploying a Bkper app from scratch. For the deep reference on any topic — architecture, configuration, development, events, or deployment — follow the links in each step.

## Prerequisites

[Development Setup](https://bkper.com/docs/platform/getting-started/setup.md) — the CLI installed and authenticated.

## Walkthrough

1. **Scaffold from the template**

    ```bash
    bkper app init my-app
    cd my-app
    ```

    `bkper app init my-app` creates `./my-app` and uses `my-app` as the app id. The CLI sets your package name, URLs, and event-handler loop guards automatically. See [App Configuration](https://bkper.com/docs/platform/apps/configuration.md) for the full `bkper.yaml` reference.

2. **Install and start developing**

    ```bash
    npm install
    npm run dev
    ```

    This runs the Vite client dev server and the local Worker runtime with automatic event tunneling. See [Development Experience](https://bkper.com/docs/platform/apps/development.md) for details.

3. **Open the app**

    Visit [http://localhost:5173](http://localhost:5173). Select a book to see account balances. No OAuth setup required — the platform handles authentication.

4. **Trigger an event**

    Go to any Bkper book and check a transaction. The event handler creates a 20% draft using the original from and to Accounts. It does not affect balances unless posted. See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for the full event model.

5. **Make a change**

    Edit `server/src/events/handlers/transaction-checked.ts` and save. The Worker reloads automatically. Check another transaction to see your change.

6. **Customize your listing**

    Update `bkper.yaml` with your app's description and owner details. Replace the placeholder logos in `client/public/images/`. See [App Listing](https://bkper.com/docs/platform/apps/app-listing.md) for publishing details.

7. **Update the README**

    Edit `README.md` for end users — what the app does and how to use it. If your app exposes `/api/*` routes for users or integrators, include the app API base URL, `/openapi.json` URL, and one minimal authenticated example. Keep deeper developer docs in `AGENTS.md`.

8. **Establish shared source**

    Review the app, create its first commit, and sync it:

    ```bash
    git add .
    git commit -m "Initial app"
    bkper app sync
    ```

    Stored Git source is required before an app can sync or deploy. For this standalone app without an external remote, sync creates the recommended private Bkper-managed source and configures it as `origin`. Authorized teammates and coding agents can then clone the same codebase with `bkper app clone my-app`. See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for access rules and external source workflows.

9. **Check and deploy**

    ```bash
    npm run check
    npm run deploy
    ```

    Deployment is explicit: syncing or pushing source does not deploy the app. Your app is live at `https://my-app.bkper.app`. See [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) for preview environments, secrets, and KV.

## Next steps

- [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) — Clone and improve one private app codebase together
- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Understand the single Worker client/server structure
- [App Configuration](https://bkper.com/docs/platform/apps/configuration.md) — Full `bkper.yaml` reference
- [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) — All event types and patterns
- [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) — Preview environments and secrets

---
source: /docs/platform/apps/overview.md

# The Bkper Platform

The Bkper Platform is a complete managed environment for building, deploying, and hosting apps on Bkper. It removes infrastructure complexity so you can focus on business logic.

### Hosting

Apps are deployed to `{appId}.bkper.app` on a global edge network powered by [Cloudflare Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/). Your app runs close to your users, with zero infrastructure to manage.

Preview environments are built in — deploy to a preview URL to test before going to production.

### App APIs

The same Worker can expose app-defined `/api/*` routes. Treat those routes as the reusable contract for your app behavior:

- The bundled web client can call them.
- Scripts, external clients, and agents can call them too.
- The default template documents them with an app OpenAPI spec at `/openapi.json`.

### AI inference

When an app needs model inference, use Bkper AI by default. An authenticated app API route or event establishes the user and app identity, then platform outbound supplies authorization and usage attribution for the Worker's Bkper AI requests. The app does not need provider credentials.

See [Add Bkper AI to an App](https://bkper.com/docs/platform/apps/ai.md) for live model discovery, strict structured output, validation, and the client-to-Worker authentication flow.

### Authentication

OAuth is pre-configured. No client IDs, no redirect URIs, no consent screens to build.

- **Web client** — Use `@bkper/web-auth`: `auth.getAccessToken()`. See [App Architecture → Client authentication](https://bkper.com/docs/platform/apps/architecture.md#client-authentication).
- **Server API routes** — Send `Authorization: Bearer <token>` to `/api/*`; dispatch validates it and platform outbound injects auth for server-side Bkper API calls. See [App Architecture → Server API authentication](https://bkper.com/docs/platform/apps/architecture.md#server-api-authentication).
- **Event handlers** — Handle `/events` in the same Worker and call Bkper with server-side `new Bkper()`; dispatch/outbound handle auth and agent identity. See [Event Handlers → Authentication](https://bkper.com/docs/platform/apps/event-handlers.md#authentication).
- **Local development** — The Vite auth middleware uses your CLI credentials. See [Development Experience → Local development authentication](https://bkper.com/docs/platform/apps/development.md#local-development-authentication).

### Services

Declare the services you need in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md) and the platform provisions them:

- **KV storage** — Key-value storage for caching and state. Access via `c.env.KV` in your handlers.
- **Secrets** — Securely stored environment variables. Set via `bkper app secrets put`, access via `c.env.SECRET_NAME`.

### Developer experience

The project template composes the full development environment:

```bash
npm run dev
```

This runs two processes concurrently: `vite dev` for the client UI (HMR), and `bkper app dev` for the Worker runtime (Miniflare for `/api/*` and `/events`, plus a Cloudflare tunnel so Bkper can route webhook events to your laptop). Your entire development environment, running locally.

### Shared app source

Bkper can host one private codebase for your app. Authorized teammates and coding agents can clone it, improve it locally, and continue building from the same shared history.

Source synchronization remains separate from deployment. A Git push stores source but never builds or deploys the app.

See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for the collaboration workflow, access rules, and external Git options.

### Deployment

Check and deploy the app template:

```bash
npm run check
npm run deploy
```

Your app is live at `{appId}.bkper.app`. The platform handles routing, SSL, and edge distribution.

## What you'd build yourself without it

Without the platform, creating a Bkper app with a UI, event handling, and authentication requires:

| Concern                  | Without the platform                                                                    | With the platform                               |
| ------------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Hosting**              | Provision servers, configure domains, SSL, CDN                                          | `bkper app deploy`                              |
| **Authentication**       | Register OAuth client, build consent screen, handle token refresh, manage redirect URIs | `auth.getAccessToken()`                         |
| **Event webhooks**       | Set up a public endpoint, configure DNS, handle JWT verification                        | Declare in `bkper.yaml`, platform routes events |
| **Local dev webhooks**   | Install ngrok or similar, manually configure tunnel URL                                 | `bkper app dev` starts tunnel automatically     |
| **Secrets**              | Set up a secrets manager, configure access                                              | `bkper app secrets put`                         |
| **KV storage**           | Deploy Redis/Memcached, manage connections                                              | Declare `KV` in `bkper.yaml`                    |
| **Preview environments** | Build a staging pipeline                                                                | `bkper app deploy --preview`                    |
| **Shared app source**    | Operate a separate private Git host                                                     | Managed source for app developers and agents    |
| **Type safety**          | Manually create type definitions                                                        | `env.d.ts` auto-generated                       |

The platform eliminates all of this. You write business logic, the platform handles infrastructure.

## Getting started

```bash
# Create a new app from the template
bkper app init my-app
cd my-app

# Install dependencies and start developing
npm install
npm run dev
```

This gives you a working app with a client UI, server API routes, and `/events` handling in one Worker — all running locally with full HMR and webhook tunneling.

## Next steps

- [Your First App](https://bkper.com/docs/platform/apps/first-app.md) — Build and deploy a complete platform app
- [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) — Collaborate from one private codebase
- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Understand how platform apps are structured

---
source: /docs/platform/apps/quality.md

# App Quality Guidelines

Use these guidelines when building, changing, or reviewing a Bkper app. They complement the detailed architecture, security, and feature documentation.

After implementation, review the changed code against these guidelines before considering the work complete. Automated checks support this review but do not replace it.

## Bkper behavior

Always:

- Model financial activity as balanced resource movements between Accounts.
- Keep calculations deterministic and cover financial behavior with focused unit tests.
- Use canonical Bkper SDK and API types instead of recreating Bkper data structures.
- Add meaning with properties before introducing new structural complexity.

## User interface

Always:

- Prefer Web Awesome for all UI controls. Do not use native controls when Web Awesome provides an equivalent.
- If no equivalent exists, first compose existing Web Awesome components. Create a reusable Lit web component only when necessary, keeping any native controls encapsulated within it.
- Style with Bkper design tokens instead of ad-hoc design constants.
- Support the active Bkper light or dark theme.
- Keep interactions accessible and provide clear loading, empty, and error states.

Apps opened from a Book context menu should feel like part of the Book. Keep their layout focused, make them work in the configured sidebar or expanded width, and preserve context when the Book URL changes.

## Startup

Always render the app shell or a meaningful loading state before waiting for authentication, API calls, or other initialization. Start asynchronous work after the first render and update the interface as results arrive.

Prefer loading only the client code and Web Awesome components needed for the initial experience.

## API contracts

When an app exposes its own HTTP API:

- Define and publish its OpenAPI contract at `/openapi.json`.
- Generate client types from that contract.
- Call the API through the generated typed boundary rather than duplicating request or response types.
- Validate untrusted request data at the server boundary.
- Keep published routes backward compatible or introduce a new API version for breaking changes.

UI-only and event-only apps without an app-owned HTTP API do not need to add one. Direct calls to Bkper should use the canonical Bkper SDK and API types.

## Focused modules

Prefer modules with one clear responsibility, high cohesion, and few dependencies.

- Components render state and communicate user intent. They should delegate API and Bkper operations to client API or service modules.
- HTTP routes and event handlers adapt transport concerns and delegate app behavior.
- Business modules contain domain decisions without depending on UI, HTTP, storage, or external-service details.
- Connectors isolate external APIs and storage concerns from business behavior.
- Add a repository or another layer when connection or storage complexity justifies it, not by default.
- Avoid layers that only forward calls without creating a useful boundary.

Split a module when unrelated behavior changes for different reasons or when mixed responsibilities make it difficult to understand or test.

## Security

Keep the security boundary simple and explicit:

- Authorize sensitive app operations on the server.
- Validate untrusted requests, events, and browser messages at their boundaries.
- Keep application secrets and privileged credentials out of client bundles.
- Access only the Book data needed for the operation.
- Make event handling safe to retry and prevent event loops.

See [App Security](https://bkper.com/docs/platform/apps/security.md) and [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for implementation details.

## Verification

After implementation:

1. Review the changed code against these guidelines.
2. Run the app's deterministic checks, including unit tests, typechecking, and production builds.
3. Confirm generated API types and contract snapshots are current when the API changed.
4. Verify user interfaces in their intended Book context, including first rendering, loading, errors, theme, and configured width.

App reviews should report concrete findings with file locations and suggested fixes. If no issues are found, a short confirmation is enough.

## Next Steps

- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Client, server, API, and event structure.
- [App Security](https://bkper.com/docs/platform/apps/security.md) — Authentication and authorization boundaries.
- [Context Menu](https://bkper.com/docs/platform/apps/context-menu.md) — Embedded Book context and open modes.
- [Development Experience](https://bkper.com/docs/platform/apps/development.md) — Local development and deterministic checks.

---
source: /docs/platform/apps/security.md

# App Security

Bkper and each platform app have separate security responsibilities. This guide explains the platform authentication boundary and common authorization checks an app must enforce.

## Authentication and authorization

Authentication identifies the Bkper user making a request. The platform handles this flow for deployed apps.

Authorization determines whether that user may perform a specific operation.

See [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) for the client and server authentication flow.

## Authorize app operations

Platform authentication identifies the Bkper user and provides outbound authentication for server-side Bkper requests. Your app must still decide which authenticated users may perform each operation. Protect sensitive data and actions in the server API; client-side checks may improve the UI, but they are not an authorization boundary.

### Restrict an internal app by user domain

For an app intended only for people in one organization, authorize the authenticated user's hosted domain:

```ts
const ALLOWED_DOMAIN = 'example.com';

const user = await context.bkper.getUser();
const domain = user.getHostedDomain()?.toLowerCase();

if (domain !== ALLOWED_DOMAIN) {
    return c.json(buildApiError('FORBIDDEN', 'This app is restricted to your organization'), 403);
}
```

### Authorize a Book-backed operation

When an operation acts on a Book, use an explicit permission allowlist appropriate to that operation. For an operation that requires edit access:

```ts
import { Permission } from 'bkper-js';

const EDIT_PERMISSIONS: readonly Permission[] = [Permission.EDITOR, Permission.OWNER];

const book = await context.bkper.getBook(bookId);

if (!EDIT_PERMISSIONS.includes(book.getPermission())) {
    return c.json(
        buildApiError('FORBIDDEN', 'Editor or owner permission required for this operation'),
        403
    );
}
```

Read, posting, and other operations may require different policies. Choose the minimum authorization appropriate to the behavior instead of treating every authenticated user as authorized.

### Require app installation

Having permission to access a Book does not mean the app is installed in that Book. If an app is only supposed to be used with Books where it is installed, verify installation:

```ts
const APP_ID = 'my-app';

const book = await context.bkper.getBook(bookId);
const installedApps = await book.getApps();
const isInstalled = installedApps.some(app => app.getId() === APP_ID);

if (!isInstalled) {
    return c.json(buildApiError('FORBIDDEN', 'This app is not installed in this Book'), 403);
}
```

## Next Steps

- [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md) — Review cross-cutting app quality and security expectations.
- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Understand client and server authentication flows.
- [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md#setting-secrets) — Store production and preview secrets.
- [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md#authentication) — Understand authentication for platform and self-hosted events.

---
source: /docs/platform/apps/self-hosted.md

# Bkper Self-Hosted Webhooks

The [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md) handles hosting, authentication, and deployment for you. However, you can host event handlers on your own infrastructure if you have specific requirements — existing cloud setup, compliance constraints, or legacy apps.

> **Tip**
> Use the Bkper Platform unless you have a specific reason to self-host. It eliminates the need to manage authentication, secrets, hosting, and deployment yourself.
## Cloud Functions

A Bkper event handler running on [Google Cloud Functions](https://cloud.google.com/functions/) receives authenticated calls from the `bkper-hrd@appspot.gserviceaccount.com` service account. You need to grant this service account the [Cloud Functions Invoker IAM role](https://cloud.google.com/functions/docs/securing/managing-access-iam) (`roles/cloudfunctions.invoker`).

Set the production endpoint in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md):

```yaml
webhookUrl: https://us-central1-my-project.cloudfunctions.net/events
```

### Authentication

An OAuth Access Token **of the user who installed the app** is sent to the production `webhookUrl` endpoint in the `bkper-oauth-token` HTTP header, along with the agent identifier in `bkper-agent-id`, on each event. Your handler uses this token to call the API back on behalf of the user.

Both production (`webhookUrl`) and development (`webhookUrlDev`) endpoints receive OAuth tokens in the `bkper-oauth-token` header.

### Throughput and scaling

Event throughput can be high, especially when processing large batches. Set the [max instance limit](https://cloud.google.com/functions/docs/max-instances#setting_max_instances_limits) — usually **1-2 is enough**. When the function returns `429 Too Many Requests`, the event is automatically retried with incremental backoff until it receives an HTTP `200`.

### Response format

The function response must follow the standard format:

```ts
{ result?: any, error?: any }
```

See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md#response-format) for details on response handling.

### Considerations

- Execution environment is subject to [Cloud Function Quotas](https://cloud.google.com/functions/quotas) — quota counts against the developer account, not the end user
- Recommended for scenarios where event throughput exceeds **1 event/second/user** and processing can be handled asynchronously
- Can be combined with context menus built with [Apps Script HTML Service](https://developers.google.com/apps-script/guides/html) or any other UI infrastructure

---

## Generic Webhooks

You can host event handlers on any infrastructure — other cloud providers, containers, on-premise servers.

Configure the same `webhookUrl` property in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md):

```yaml
webhookUrl: https://my-server.example.com/bkper/events
```

### Authentication

Calls to the production webhook URL are signed with a JWT token using the [Service to Function](https://cloud.google.com/functions/docs/securing/authenticating#service-to-function) method. You can verify this token to assert the identity of the Bkper service.

> **Note**
> Cloud Functions handles JWT verification automatically. For other infrastructure, you need to implement verification yourself. We strongly recommend Cloud Functions for this reason.
### Retry behavior

If your infrastructure returns an HTTP `429` status, the event is automatically retried with incremental backoff until it receives an HTTP `200`. Use this to handle temporary overload gracefully.

---
source: /docs/platform/apps/shared-app-source.md

# Shared App Source

Bkper-managed app source gives your team and coding agents one shared private codebase for a Bkper app. Authorized app developers can clone the same repository, improve it locally, and continue from one shared history without setting up a separate Git host.

Every app sync and deployment requires clean, committed source stored in a durable Git remote. Bkper-managed source is the recommended default for development collaboration. It does not automatically build or deploy your app.

## Who can access the source

The app owner and users matched by the `developers` field in `bkper.yaml` can read and update managed source. This includes configured domain patterns such as `*@example.com`.

App users and Book collaborators do not receive source access unless they also match the app's developer policy.

## Start a shared codebase

A standalone app becomes eligible for Bkper-managed source when:

- `bkper.yaml` is at the root of its Git repository.
- The current branch is `main` for the first managed sync.
- The working tree is clean and has at least one commit.
- No Git remote is configured.

If the app source already exists but is not yet versioned, do not run `bkper app init` again. From the directory containing `bkper.yaml`, initialize it and review the files before committing. Update `.gitignore` first so local secrets, dependencies, and build output are not staged.

```bash
git init -b main

# Review files and update .gitignore before staging
git status --short

git add .

# Verify exactly what will be committed
git status --short
git commit -m "Initial app"
bkper app sync
```

For a new app, `bkper app init` initializes Git on `main`, but it does not create the first commit. Review and commit the app before its first sync:

```bash
bkper app init my-app
cd my-app

# Review the generated app, then commit it
git add .
git commit -m "Initial app"

bkper app sync
```

For an eligible new or existing app, `bkper app sync` creates its private Bkper-managed source and configures it as `origin`. The same command also syncs app metadata from `bkper.yaml`.

## Clone and continue together

Any authorized app developer can start from the shared codebase:

```bash
bkper app clone <appId>
cd <appId>
npm install
```

`bkper app clone` copies the repository but does not install dependencies or run repository scripts. A teammate or coding agent can then work in the local clone, run the project's checks, commit changes, and push them back to the shared repository. Another authorized developer can pull or clone that history and continue the work.

Keep agent instructions such as `AGENTS.md` in the repository so every teammate and coding agent starts with the same project context and safety rules.

## Source synchronization is not deployment

Source storage and app deployment are separate operations. Apps without a Git repository cannot sync or deploy; the CLI provides the initialization and managed-sync steps needed to establish source safely.

| Action             | What it does with source                                                       | Does it deploy? |
| ------------------ | ------------------------------------------------------------------------------ | --------------- |
| `git push`         | Stores committed source in the managed repository.                             | No              |
| `bkper app sync`   | Safely pushes managed source, then syncs app metadata from `bkper.yaml`.       | No              |
| `npm run build`    | Creates local build output in `dist/`.                                         | No              |
| `bkper app deploy` | Pushes and verifies the managed commit, then uploads the existing local build. | Yes             |

An ordinary Git push never deploys. `bkper app deploy` also does not run a build, so build locally before deploying the result you intend to release. The CLI verifies the stored source commit but does not prove that the local `dist/` output was built from it.

Managed sync and deploy require a clean, committed working tree and use fast-forward safety checks. External sync and deploy require the current branch to track an upstream containing the current clean commit. The CLI does not automatically commit, merge, rebase, force-push, reset, discard files, choose an external remote, or push to an external provider.

## External Git and monorepos

Bkper-managed source is optional. Existing workflows remain external when:

- the app already has a GitHub, GitLab, or other provider remote; or
- `bkper.yaml` is inside a monorepo rather than at the repository root.

The current branch must have a configured upstream containing the commit being synced or deployed. If no upstream is configured, choose the intended provider remote and store the branch explicitly:

```bash
git push --set-upstream <remote> <branch>
```

The CLI fetches and verifies the upstream without pushing or changing the working tree. Clone external apps from their provider. `bkper app clone` is for Bkper-managed source only.

Moving an existing standalone app from an external provider is intentional: the CLI never removes or renames an existing remote. Before changing remotes, make sure every branch and tag you want to preserve is available locally and the current `main` branch is clean and committed. Removing all external remotes and running `bkper app sync` then activates managed source for an eligible app.

The `repoUrl` field in `bkper.yaml` is app-listing metadata. It does not select managed or external source mode.

## Next steps

- [Your First App](https://bkper.com/docs/platform/apps/first-app.md) — Scaffold an app and establish its shared source
- [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) — Build, sync, preview, and deploy explicitly
- [CLI](https://bkper.com/docs/platform/tools/cli.md) — Install the CLI and review its app-development workflows
- [Coding Agents](https://bkper.com/docs/ai/coding-agents.md) — Give coding agents the Bkper and project context they need

---
source: /docs/platform/examples.md

# Examples & Patterns

These are production apps built on Bkper, each demonstrating a different integration pattern. All are open source and available on GitHub.

## Tax Bot

[GitHub](https://github.com/bkper/bkper-tax-bot)

Calculates VAT, GST, and other taxes automatically when transactions are posted. Demonstrates **property-driven configuration** — tax rates and rules are stored in account and group properties, making the bot configurable per-book without code changes.

**What you'll learn:** Using account/group properties to drive behavior, creating related transactions automatically, working with transaction amounts.

## Exchange Bot

[GitHub](https://github.com/bkper/bkper-exchange-bot)

Converts transaction amounts between Books based on updated exchange rates and calculates realized gains and losses. Demonstrates **multi-book synchronization** — when a transaction is checked in one book, the bot creates corresponding entries in connected books.

**What you'll learn:** Mirroring transactions between books, working with exchange rates, gain/loss calculations, cross-book data flow.

## Inventory Bot

[GitHub](https://github.com/bkper/bkper-inventory-bot)

Calculates COGS (Cost of Goods Sold) automatically using FIFO method when inventory items are sold. Demonstrates **inventory management patterns** — tracking purchase and sale quantities to compute accurate costs.

**What you'll learn:** Inventory management patterns, purchase/sale quantity tracking, automatic COGS calculation.

## Subledger Bot

[GitHub](https://github.com/bkper/bkper-subledger-bot)

Manages hierarchical relationships between parent and subsidiary books, mapping accounts and groups across ledger levels. Demonstrates **hierarchical ledger relationships** — keeping consolidated and detailed views in sync.

**What you'll learn:** Parent-child book patterns, account/group mapping between books, consolidated reporting.

## Bkper Sheets

[GitHub](https://github.com/bkper/bkper-sheets)

The Google Sheets Add-on — extends Bkper with custom spreadsheet functions, data import/export, and formula-driven reporting. Demonstrates a full **Google Workspace integration**.

**What you'll learn:** Sheets add-on architecture, custom functions, data synchronization between Bkper and Sheets.

---
source: /docs/platform/getting-started/agent-model.md

# The Agent Model

When you build something that interacts with Bkper — a script, an automation, a full platform app, or even a bank integration — Bkper treats it as an **agent**: any application that can perform actions on books **on behalf of a user**.

These agents can take various forms such as Apps, Bots, Assistants, or even Banks that interact with your books:

<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; align-items: center;">
  <div style="padding-bottom: 20px;">
    [Image: Bkper Agents Model]
  </div>
  <div style="padding-bottom: 20px;">
    <div style="position: relative; padding-bottom: 70.25%; height: 0; overflow: hidden;">
      <iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 4px solid lightgrey; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);" src="https://www.youtube.com/embed/ZZ2QUCePgYw" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
    </div>
  </div>
</div>

## Permissions

Agents can only access books that have been explicitly shared with the user they're acting on behalf of. Your code never has elevated access — it operates within the same permission boundaries as the human user who authorized it.

## Identity

Every API request your app makes includes a `bkper-agent-id` header. This lets Bkper attribute actions to the correct agent, so activities and transactions appear with your app's logo and name throughout the Bkper interface — making it easy for book owners to see which entity performed specific actions:

![Agents on Bkper](https://bkper.com/docs/_astro/bkper-app-agents.Dse93XFf.png)

## Bots vs AI Agents

The distinction between a "bot" and an "AI agent" is about capability, not a different type of Bkper primitive. Both are just apps:

| | **Bot** | **AI Agent** |
| --- | --- | --- |
| **Purpose** | Automating predefined tasks | Autonomously perform tasks |
| **Capabilities** | Follows rules; limited learning; basic interactions | Complex, multi-step actions; learns and adapts; makes decisions independently |
| **Interaction** | Reactive; responds to triggers or commands | Proactive; goal-oriented |

In Bkper, what people call "bots" are typically apps whose primary capability is [event handling](https://bkper.com/docs/platform/apps/event-handlers.md) — reacting to things that happen in a book. AI agents go further, combining event handling with LLM reasoning to make decisions.

---
source: /docs/platform/getting-started/quick-wins.md

# Quick Wins

You've [set up your environment](https://bkper.com/docs/platform/getting-started/setup.md). Here are three ways to start building immediately — from a 1-line shell command to a 20-line script.

## CLI piping

Copy all accounts from one book to another in a single line:

```bash
bkper account list -b $SOURCE_BOOK --format json | bkper account create -b $DEST_BOOK
```

The CLI outputs JSON that feeds directly into the next command. No code, no setup beyond the CLI itself.

Add a property to every matching transaction:

```bash
bkper transaction list -b $BOOK -q "account:Expenses" --format json | \
  bkper transaction update -b $BOOK -p "reviewed=true"
```

See [CLI Scripting & Piping](https://bkper.com/docs/platform/scripts/cli-pipelines.md) for more patterns.

## Node.js script

A short script that lists all accounts with their current balances:

```ts
import { Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});

const bkper = new Bkper();
const book = await bkper.getBook('your-book-id');
const report = await book.getBalancesReport('');
const containers = report.getBalancesContainers();

for (const container of containers) {
    console.log(`${container.getName()}: ${container.getCumulativeBalance()}`);
}
```

Run it:

```bash
npm install bkper-js bkper
node script.mjs
```

See [Node.js Scripts](https://bkper.com/docs/platform/scripts/node-scripts.md) for more examples.

## Direct API call

Call the REST API from any language. Here's a `curl` example:

```bash
# Get your OAuth token (after running bkper auth login)
TOKEN=$(bkper auth token)

# List your books
curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.bkper.app/v5/books | jq '.items[].name'
```

See [Direct API Usage](https://bkper.com/docs/platform/scripts/rest-api.md) for the full guide.

## What next?

These quick wins are just the beginning. Depending on what you want to build:

- **More automation** — [CLI Scripting & Piping](https://bkper.com/docs/platform/scripts/cli-pipelines.md) for shell-based workflows, [Node.js Scripts](https://bkper.com/docs/platform/scripts/node-scripts.md) for complex logic
- **A full app** — [Your First App](https://bkper.com/docs/platform/apps/first-app.md) to build and deploy an app with UI and event handling on the [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md)
- **Google Workspace** — [Apps Script Development](https://bkper.com/docs/platform/google-workspace/apps-script.md) for Sheets automation and triggers

---
source: /docs/platform/getting-started/setup.md

# Bkper Developer Setup

Everything you build on Bkper starts with the CLI. It handles authentication, provides the `bkper-js` library for programmatic access, and manages the full app lifecycle.

## Prerequisites

- [Node.js](https://nodejs.org/) >= 22.19.0

## Install and authenticate

1. **Install the CLI**

   ```bash
   npm i -g bkper
   ```

2. **Authenticate**

   ```bash
   bkper auth login
   ```

   This opens your browser for Google OAuth authentication. Once complete, the CLI stores your credentials locally. All API calls — from the CLI, from scripts, and from `bkper-js` — use this token. To clean up later, run `bkper auth logout`, which revokes the stored refresh token when possible and clears local credentials.

3. **Verify**

   ```bash
   bkper book list
   ```

   You should see a list of your Bkper Books. If you do, you're ready to build.

## What you now have

After setup, you have:

- **CLI commands** — Manage books, accounts, transactions, and apps from the terminal. Run `bkper --help` for the full command list.
- **Auth provider for scripts** — Use `getOAuthToken()` from the `bkper` package in any Node.js script:

  ```ts
  import { Bkper } from 'bkper-js';
  import { getOAuthToken } from 'bkper';

  Bkper.setConfig({
      oauthTokenProvider: async () => getOAuthToken(),
  });
  ```

- **App development tools** — Initialize, develop, and deploy platform apps with `bkper app` commands.

## Optional: API key

For dedicated API quota and project-level usage tracking, you can configure your own API key. This is optional — the default shared quota (60 requests per minute) works for most use cases.

See [Direct API Usage](https://bkper.com/docs/platform/scripts/rest-api.md#custom-api-key) for setup instructions.

## Next steps

- [Quick Wins](https://bkper.com/docs/platform/getting-started/quick-wins.md) — The fastest ways to create value with Bkper programmatically
- [Your First App](https://bkper.com/docs/platform/apps/first-app.md) — Build and deploy a platform app
- [CLI reference](https://bkper.com/docs/platform/tools/cli.md) — Overview of CLI capabilities

---
source: /docs/platform/google-workspace/apps-script.md

# Apps Script Development

[Google Apps Script](https://developers.google.com/apps-script) is Google's serverless platform for extending Google Workspace. With the `bkper-gs` library, you can build Bkper automations that run inside Google's infrastructure — no servers, no deployment pipeline, and native access to Sheets, Drive, Calendar, and Gmail.

## When to use Apps Script

Use Apps Script when your automation lives in the Google Workspace ecosystem:

- Scheduled jobs that read from or write to Google Sheets
- Spreadsheet triggers (on-edit, on-form-submit) that record transactions
- Custom add-ons distributed to a team or domain
- Workflows that combine Bkper with other Google services (Drive, Calendar, Gmail)

If you need real-time event handling, a web UI, or automation that runs outside Google Workspace, use [Node.js scripts](https://bkper.com/docs/platform/scripts/node-scripts.md) or a [platform app](https://bkper.com/docs/platform/apps/overview.md) instead.

### Add the library

`bkper-gs` is published as an Apps Script library. To add it to your script:

1. Open your script in the [Apps Script editor](https://script.google.com)
2. Click **+** next to **Libraries** in the left-side panel
3. In the "Script ID" field, enter:
   ```
   1hMJszJGSUVZDB3vmsWrUZfRhY1UWbhS0SQ6Lzl06gm1zhBF3ioTM7mpJ
   ```
4. Click **Look up**, choose the latest version, and click **Add**

The `BkperApp` global is now available in your script.

### TypeScript definitions

For TypeScript development with autocomplete, install the type definitions:

```bash
npm i -S @bkper/bkper-gs-types
```

Configure `tsconfig.json`:

```json
{
  "compilerOptions": {
    "typeRoots": ["node_modules/@bkper", "node_modules/@types"]
  }
}
```

See [Develop Apps Script using TypeScript](https://developers.google.com/apps-script/guides/typescript) and use [clasp](https://github.com/google/clasp) to push TypeScript projects to Apps Script.

## The BkperApp entry point

`BkperApp` works the same way as `CalendarApp`, `DocumentApp`, and `SpreadsheetApp` — it's a global entry point that follows familiar Apps Script conventions.

The book ID comes from the URL when you open a book at [bkper.com](https://bkper.com):

```js
// Get a book by its ID (from the URL)
const book = BkperApp.getBook('agtzfmJrcGVyLWhyZHIOCxIGTGVkZ2VyGNKJAgw');
```

### Get a book

```js
function getBookName() {
  const book = BkperApp.getBook('agtzfmJrcGVyLWhyZHIOCxIGTGVkZ2VyGNKJAgw');
  Logger.log(book.getName());
}
```

### Record a transaction

```js
function recordTransaction() {
  const book = BkperApp.getBook('agtzfmJrcGVyLWhyZHIOCxIGTGVkZ2VyGNKJAgw');
  book.record('#gas 63.23');
}
```

Transactions use the same [shorthand syntax](https://bkper.com/docs/guides/using-bkper/record-transactions.md) you'd use in the Bkper UI.

### Batch record transactions

For bulk operations, pass an array. The library sends all records in a single API call — important for avoiding Apps Script execution time limits:

```js
function importExpenses() {
  const book = BkperApp.getBook('agtzfmJrcGVyLWhyZHIOCxIGTGVkZ2VyGNKJAgw');

  const transactions = [
    '#breakfast 15.40',
    '#lunch 27.45',
    '#dinner 35.86',
  ];

  book.record(transactions);
}
```

### Query transactions

The `getTransactions()` method returns a `TransactionIterator` for handling large datasets without loading everything into memory:

```js
function listTransactions() {
  const book = BkperApp.getBook('agtzfmJrcGVyLWhyZHIOCxIGTGVkZ2VyGNKJAgw');

  const iterator = book.getTransactions("account:'Bank' after:01/01/2024");

  while (iterator.hasNext()) {
    const transaction = iterator.next();
    Logger.log(transaction.getDescription());
  }
}
```

See [Querying Transactions](https://bkper.com/docs/guides/using-bkper/query-transactions.md) for the full query syntax.

### List accounts with balances

```js
function listAccountBalances() {
  const book = BkperApp.getBook('agtzfmJrcGVyLWhyZHIOCxIGTGVkZ2VyGNKJAgw');

  const accounts = book.getAccounts();
  for (const account of accounts) {
    if (account.isPermanent() && account.isActive()) {
      Logger.log(`${account.getName()}: ${account.getBalance()}`);
    }
  }
}
```

## Building triggers

Apps Script triggers let your automation run on a schedule or respond to spreadsheet events — without any always-on infrastructure.

### Time-based (scheduled)

```js
function setupDailySync() {
  ScriptApp.newTrigger('syncTransactions')
    .timeBased()
    .everyDays(1)
    .atHour(6)
    .create();
}

function syncTransactions() {
  const book = BkperApp.getBook('YOUR_BOOK_ID');
  const sheet = SpreadsheetApp.openById('YOUR_SHEET_ID').getActiveSheet();

  // Read rows from Sheets, record to Bkper
  const rows = sheet.getDataRange().getValues();
  const transactions = rows.slice(1).map(row => `${row[0]} ${row[1]} ${row[2]}`);
  book.record(transactions);
}
```

### Spreadsheet edit trigger

```js
function onEdit(e) {
  const sheet = e.source.getActiveSheet();
  if (sheet.getName() !== 'Expenses') return;

  const row = e.range.getRow();
  const amount = sheet.getRange(row, 3).getValue();
  const description = sheet.getRange(row, 2).getValue();

  if (amount && description) {
    const book = BkperApp.getBook('YOUR_BOOK_ID');
    book.record(`${description} ${amount}`);
  }
}
```

## TypeScript development workflow

For non-trivial scripts, use [clasp](https://github.com/google/clasp) for local development with TypeScript:

```bash
# Install clasp
npm install -g @google/clasp

# Log in
clasp login

# Clone an existing script
clasp clone <scriptId>

# Push changes
clasp push

# Watch for changes
clasp push --watch
```

With `@bkper/bkper-gs-types` configured, your editor provides full autocomplete for `BkperApp`, `Book`, `Transaction`, `Account`, and all other bkper-gs types.

## API reference

The complete `bkper-gs` reference is at [bkper.com/docs/bkper-gs](https://bkper.com/docs/bkper-gs/).

## Related

- [Building Sheets Integrations](https://bkper.com/docs/platform/google-workspace/google-sheets.md) — Custom Sheets automations with bkper-gs
- [Node.js Scripts](https://bkper.com/docs/platform/scripts/node-scripts.md) — When you need automation outside Google Workspace
- [Guides → Google Sheets Add-on](https://bkper.com/docs/guides/google-sheets.md) — End-user guide for recording and fetching data with the Bkper add-on

---
source: /docs/platform/google-workspace/google-sheets.md

# Building Sheets Integrations

The [Bkper Add-on for Google Sheets](https://bkper.com/docs/guides/google-sheets.md) lets users record transactions and fetch data with built-in functions. This page covers the next level: building *custom* Sheets integrations with `bkper-gs` — automated pipelines, custom menus, scheduled reports, and two-way sync.

See [Apps Script Development](https://bkper.com/docs/platform/google-workspace/apps-script.md) first to set up `bkper-gs` and understand the fundamentals.

## The boundary: add-on vs custom integrations

The built-in add-on covers the common cases well. Build a custom integration when:

- You need a **custom menu** tailored to your team's workflow
- You want **automated pipelines** that run on a schedule without user interaction
- You're building a **specialized report** that the standard functions don't cover
- You need **two-way sync** between a spreadsheet and Bkper (data flowing both directions)
- You're distributing a **custom add-on** to your domain or organization

## Custom menu functions

Add a Bkper-powered menu to any Google Sheet. Users can trigger operations directly from the spreadsheet without opening Bkper.

```js
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Bkper')
    .addItem('Import expenses from this sheet', 'importExpenses')
    .addItem('Fetch account balances', 'fetchBalances')
    .addSeparator()
    .addItem('Sync all', 'syncAll')
    .addToUi();
}

function importExpenses() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Expenses');
  const book = BkperApp.getBook(getBookId());

  const rows = sheet.getDataRange().getValues().slice(1); // skip header
  const transactions = rows
    .filter(row => row[0] && row[1] && row[2])     // date, description, amount
    .map(row => `${row[1]} ${row[2]} ${row[0]}`);  // "description amount date"

  book.record(transactions);
  SpreadsheetApp.getUi().alert(`Imported ${transactions.length} transactions.`);
}
```

### Sheets → Bkper (import)

Pull structured data from a spreadsheet and create transactions in bulk. Useful for importing bank exports, expense reports, or any data that lives in Sheets first.

```js
function importFromSheet() {
  const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
  const sheet = ss.getSheetByName('Transactions');
  const book = BkperApp.getBook('YOUR_BOOK_ID');

  const rows = sheet.getDataRange().getValues();
  const header = rows[0];
  const dateCol = header.indexOf('Date');
  const descCol = header.indexOf('Description');
  const amountCol = header.indexOf('Amount');
  const importedCol = header.indexOf('Imported');

  const toImport = [];

  for (let i = 1; i < rows.length; i++) {
    const row = rows[i];
    if (row[importedCol]) continue; // skip already imported

    const date = Utilities.formatDate(new Date(row[dateCol]), 'UTC', 'dd/MM/yyyy');
    toImport.push({
      row: i + 1,
      tx: `${row[descCol]} ${row[amountCol]} ${date}`,
    });
  }

  if (toImport.length === 0) return;

  book.record(toImport.map(item => item.tx));

  // Mark rows as imported
  for (const item of toImport) {
    sheet.getRange(item.row, importedCol + 1).setValue(true);
  }
}
```

### Bkper → Sheets (export/reporting)

Write Bkper data into a spreadsheet for dashboards, analysis, or sharing with stakeholders who work in Sheets.

```js
function exportBalancesToSheet() {
  const book = BkperApp.getBook('YOUR_BOOK_ID');
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Balances');

  sheet.clearContents();
  sheet.appendRow(['Account', 'Balance']);

  const accounts = book.getAccounts();
  for (const account of accounts) {
    if (account.isPermanent() && account.isActive()) {
      sheet.appendRow([account.getName(), account.getBalance()]);
    }
  }
}
```

## Scheduled reporting

Use time-based triggers to run reports on a schedule — no user needs to be logged in.

```js
function setupWeeklyReport() {
  // Run every Monday at 8am
  ScriptApp.newTrigger('generateWeeklyReport')
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(8)
    .create();
}

function generateWeeklyReport() {
  const book = BkperApp.getBook('YOUR_BOOK_ID');
  const ss = SpreadsheetApp.openById('YOUR_REPORT_SHEET_ID');
  const sheet = ss.getSheetByName('Weekly') || ss.insertSheet('Weekly');

  const lastWeek = new Date();
  lastWeek.setDate(lastWeek.getDate() - 7);
  const from = Utilities.formatDate(lastWeek, 'UTC', 'MM/dd/yyyy');

  sheet.clearContents();
  sheet.appendRow(['Description', 'Amount', 'Date', 'Account']);

  const iterator = book.getTransactions(`after:${from}`);
  while (iterator.hasNext()) {
    const tx = iterator.next();
    sheet.appendRow([
      tx.getDescription(),
      tx.getAmount(),
      tx.getDateFormatted(),
      tx.getCreditAccount()?.getName(),
    ]);
  }
}
```

## Working with Custom Properties

Custom Properties let you attach metadata to Bkper entities (accounts, transactions). Use them as a sync key between Sheets and Bkper to avoid duplicates and enable updates.

```js
// Store a Sheets row ID on a transaction as a custom property
function recordWithSheetId(book, txString, sheetRowId) {
  const transaction = book.newTransaction()
    .setDate(new Date())
    .setAmount(100)
    .setDescription(txString)
    .setProperty('sheet_row_id', sheetRowId);

  transaction.create();
}

// Later, look up transactions by their sheet row ID
function findBySheetId(book, sheetRowId) {
  const iterator = book.getTransactions(`properties.sheet_row_id:${sheetRowId}`);
  return iterator.hasNext() ? iterator.next() : null;
}
```

This pattern enables idempotent sync: check if a transaction already exists before creating it, and update rather than duplicate.

## When to move beyond Sheets

Google Sheets is powerful, but it has limits. Consider a [platform app](https://bkper.com/docs/platform/apps/overview.md) when:

- You need **real-time event handling** — platform apps get webhook events pushed instantly; Sheets triggers have latency and quota limits
- You need **a web UI** outside of Sheets — platform apps get `{appId}.bkper.app` with full auth
- Your automation needs to **run at scale** — Workers have no cold starts and higher execution limits than Apps Script
- You want to **publish to all Bkper users** — platform apps appear in the Bkper app listing; Sheets add-ons have a separate distribution model

## Related

- [Apps Script Development](https://bkper.com/docs/platform/google-workspace/apps-script.md) — Setting up `bkper-gs`, BkperApp patterns, triggers
- [The Bkper Platform](https://bkper.com/docs/platform/apps/overview.md) — When to build a full platform app
- [Guides → Google Sheets Add-on](https://bkper.com/docs/guides/google-sheets.md) — End-user guide for the built-in add-on

---
source: /docs/platform/scripts/cli-pipelines.md

# CLI Scripting & Piping

The Bkper CLI is designed for scripting. Every command supports multiple output formats, and selected write commands accept piped JSON input — making it easy to build data pipelines, batch operations, and automated workflows.

## Output formats

All commands support three output formats via the `--format` global flag:

| Format | Flag                       | Best for                                |
| ------ | -------------------------- | --------------------------------------- |
| Table  | `--format table` (default) | Human reading in the terminal           |
| JSON   | `--format json`            | Programmatic access, single-item detail |
| CSV    | `--format csv`             | Spreadsheets, AI agents, data pipelines |

```bash
# Table output (default)
bkper account list -b abc123

# JSON output
bkper account list -b abc123 --format json

# CSV output -- raw data, no truncation, RFC 4180
bkper account list -b abc123 --format csv
```

**CSV output details:**

- RFC 4180 compliant — proper quoting, CRLF line endings, no truncation
- All metadata included — IDs, properties, hidden properties, URLs, and timestamps
- Raw values — dates in ISO format, numbers unformatted

> **Tip: AI agent guidance**
> When using the CLI from an AI agent or automated script, prefer `--format csv` for list commands, `--format json` for single-item commands (`get`, `create`, `update`), and stdin piping for batch operations.
## Query semantics quick reference

Use these rules in scripts to avoid ambiguous or empty results:

- `on:` supports year, month, and day (`on:2025`, `on:2025-01`, `on:2025-01-31`).
- `after:` is **inclusive** and `before:` is **exclusive**.
- A full-year range uses next-year boundary:
    - `after:2025-01-01 before:2026-01-01`

```bash
# Full year with on:
bkper transaction list -b $BOOK_ID -q "on:2025" --format csv

# Same full year with explicit boundaries
bkper transaction list -b $BOOK_ID -q "after:2025-01-01 before:2026-01-01" --format csv
```

## Batch operations

Write commands (`account create`, `transaction create`, `transaction update`) accept JSON piped via stdin. The input format follows the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts) — a single JSON object or an array of objects.

Groups are created explicitly with `bkper group create --name` and optional `--parent`, so hierarchy stays deterministic.

### Creating in batch

```bash
# Create transactions from JSON
echo '[{
  "date": "2025-01-15",
  "amount": "100.50",
  "creditAccount": {"name": "Bank Account"},
  "debitAccount": {"name": "Office Supplies"},
  "description": "Printer paper",
  "properties": {"invoice": "INV-001"}
}]' | bkper transaction create -b abc123

# Create accounts
echo '[{"name":"Cash","type":"ASSET"},{"name":"Revenue","type":"INCOMING"}]' | \
  bkper account create -b abc123

# Create a group explicitly
bkper group create -b abc123 --name "Fixed Costs" --hidden

# Pipe from any script that outputs JSON
python export_bank.py | bkper transaction create -b abc123
```

Batch results are output as a flat JSON array:

```bash
bkper account create -b abc123 < accounts.json
# [{"id":"acc-abc","name":"Cash",...}, {"id":"acc-def","name":"Revenue",...}]
```

### Adding properties via CLI flag

The `--property` flag can add or override properties from the stdin payload:

```bash
echo '[{"name":"Cash","type":"ASSET"}]' | \
  bkper account create -b abc123 -p "region=LATAM"
```

## Piping between commands

All JSON output is designed to feed directly into other commands. This is the most powerful pattern — combining commands into pipelines:

### Copy data between books

```bash
# Copy all accounts from one book to another
bkper account list -b $BOOK_A --format json | bkper account create -b $BOOK_B

# Copy transactions matching a query
bkper transaction list -b $BOOK_A -q "after:2025-01-01" --format json | \
  bkper transaction create -b $BOOK_B
```

Recreate groups explicitly with `bkper group create --name ... --parent ...` before copying accounts that reference them.

### Clone a full chart of accounts

```bash
# Recreate the group hierarchy explicitly
bkper group create -b $DEST --name "Assets"
bkper group create -b $DEST --name "Current Assets" --parent "Assets"

# Then copy accounts and transactions
bkper account list -b $SOURCE --format json | bkper account create -b $DEST
bkper transaction list -b $SOURCE -q "after:2025-01-01" --format json | \
  bkper transaction create -b $DEST
```

### Batch updates with jq

Use [jq](https://jqlang.github.io/jq/) to transform data between commands:

```bash
# List transactions, modify descriptions, pipe back to update
bkper transaction list -b $BOOK -q "after:2025-01-01" --format json | \
  jq '[.[] | .description = "Updated: " + .description]' | \
  bkper transaction update -b $BOOK

# Add a property to all matching transactions
bkper transaction list -b $BOOK -q "account:Expenses" --format json | \
  bkper transaction update -b $BOOK -p "reviewed=true"

# Batch update checked transactions
bkper transaction list -b $BOOK -q "is:checked after:2025-01-01" --format json | \
  bkper transaction update -b $BOOK --update-checked -p "migrated=true"
```

### Daily export

```bash
#!/bin/bash
# Export yesterday's transactions to CSV
DATE=$(date -d "yesterday" +%Y-%m-%d)
bkper transaction list -b $BOOK_ID \
  -q "on:$DATE" \
  --format csv > "export-$DATE.csv"
```

### Bulk categorization

```bash
#!/bin/bash
# Add a property to all uncategorized transactions
bkper transaction list -b $BOOK_ID \
  -q "account:Uncategorized" \
  --format json | \
  bkper transaction update -b $BOOK_ID -p "needs_review=true"
```

## Combining with other tools

The CLI works with standard Unix tools:

```bash
# Count transactions matching a query
bkper transaction list -b $BOOK_ID -q "after:2025-01-01" --format json | jq 'length'

# Extract specific fields with jq
bkper account list -b $BOOK_ID --format json | \
  jq '[.[] | {name, type}]'

# Sort by amount
bkper transaction list -b $BOOK_ID -q "after:1900-01-01" --format json | \
  jq 'sort_by(.amount | tonumber) | reverse'
```

## Full CLI reference

For the complete command reference including all options, see the [bkper-cli app page](https://bkper.com/apps/bkper-cli.md) or run `bkper --help`.

---
source: /docs/platform/scripts/node-scripts.md

# Node.js Scripts

For tasks that go beyond CLI piping — complex logic, external API integration, scheduled jobs — write a Node.js script with `bkper-js`.

## Setup

```bash
# Create a script project
mkdir my-bkper-script && cd my-bkper-script
npm init -y
npm install bkper-js bkper
```

Authenticate once via the CLI:

```bash
bkper auth login
```

## The pattern

Every script follows the same structure: authenticate, get a book, do work.

```ts
import { Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});

const bkper = new Bkper();
const book = await bkper.getBook('your-book-id');

// Your logic here
```

### Export balances to JSON

```ts
import { Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';
import { writeFileSync } from 'fs';

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});

const bkper = new Bkper();
const book = await bkper.getBook('your-book-id');

const report = await book.getBalancesReport('on:2025-12-31');
const rows = report.getBalancesContainers().map(container => ({
    name: container.getName(),
    balance: container.getCumulativeBalance().toString(),
}));

writeFileSync('balances.json', JSON.stringify(rows, null, 2));
console.log(`Exported ${rows.length} balances`);
```

### Bulk-create accounts from a JSON export

```ts
import { Account, Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';
import { readFileSync } from 'fs';

type AccountInput = {
    name: string;
    type: 'ASSET' | 'LIABILITY' | 'INCOMING' | 'OUTGOING';
    groups?: Array<{ id?: string; name?: string }>;
};

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});

const bkper = new Bkper();
const book = await bkper.getBook('your-book-id');

const data: AccountInput[] = JSON.parse(readFileSync('accounts.json', 'utf-8'));

const accounts = data.map(acc => {
    const account = new Account(book).setName(acc.name).setType(acc.type);
    if (acc.groups?.length) {
        account.setGroups(acc.groups);
    }
    return account;
});

const created = await book.batchCreateAccounts(accounts);
console.log(`Created ${created.length} accounts`);
```

### Query transactions and generate a report

```ts
import { Amount, Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});

const bkper = new Bkper();
const book = await bkper.getBook('your-book-id');

const result = await book.listTransactions('account:Expenses after:2025-01-01');

let total = new Amount(0);
for (const tx of result.getItems()) {
    const amount = tx.getAmount();
    if (!amount) continue;

    total = total.plus(amount);
    console.log(`${tx.getDate()} | ${tx.getDescription()} | ${amount.toString()}`);
}
console.log(`\nTotal: ${total.toString()}`);
```

### With cron

```bash
# Run daily at 8am
0 8 * * * cd /path/to/script && node export.mjs
```

### In CI environments

The CLI helper `getOAuthToken()` is designed for local developer machines, where the CLI can store and refresh credentials. In GitHub Actions or other unattended environments, provide your own token source:

```ts
import { Bkper } from 'bkper-js';

const bkper = new Bkper({
    oauthTokenProvider: async () => {
        const token = process.env.BKPER_OAUTH_TOKEN;
        if (!token) {
            throw new Error('BKPER_OAUTH_TOKEN is not set');
        }
        return token;
    },
});
```

Use this pattern only when another system is already issuing and rotating the token for the job. For unattended long-running automation, implement your own OAuth flow instead of relying on the CLI's locally stored credentials.

## Error handling

Wrap API calls with proper error handling:

```ts
import { BkperError } from 'bkper-js';

try {
    const book = await bkper.getBook('your-book-id');
    // ...
} catch (error) {
    if (error instanceof BkperError && error.code === 404) {
        console.error('Book not found');
    } else if (error instanceof BkperError && error.code === 403) {
        console.error('No access to this book');
    } else if (error instanceof Error) {
        console.error('API error:', error.message);
    } else {
        console.error('Unknown error:', String(error));
    }
    process.exit(1);
}
```

## When to use scripts vs apps

| Scenario                                       | Use                                       |
| ---------------------------------------------- | ----------------------------------------- |
| One-off data migration                         | Script                                    |
| Scheduled export/report                        | Script                                    |
| Reacting to book events in real time           | [Platform app](https://bkper.com/docs/platform/apps/overview.md) |
| Custom UI for users                            | [Platform app](https://bkper.com/docs/platform/apps/overview.md) |
| Complex multi-step workflow with external APIs | Script or app, depending on trigger       |

## Next steps

- [bkper-js API Reference](https://bkper.com/docs/api/bkper-js.md) — Full SDK documentation
- [CLI Scripting & Piping](https://bkper.com/docs/platform/scripts/cli-pipelines.md) — For simpler tasks, the CLI may be enough
- [Direct API Usage](https://bkper.com/docs/platform/scripts/rest-api.md) — Use the REST API from any language

---
source: /docs/platform/scripts/rest-api.md

# Bkper REST API Authentication and Direct Usage

The Bkper REST API is the universal interface for interacting with Bkper Books. Every library and tool — [bkper-js](https://bkper.com/docs/platform/tools/libraries.md#bkper-js), [bkper-gs](https://bkper.com/docs/platform/tools/libraries.md#bkper-gs), the [CLI](https://bkper.com/docs/platform/tools/cli.md) — is built on top of it. This page shows how to call it directly from any language.

If you are using an official SDK, see the library-specific guides instead:

- **Node.js / CLI** — [Node.js Scripts](https://bkper.com/docs/platform/scripts/node-scripts.md)
- **Browser apps** — [Platform Apps](https://bkper.com/docs/platform/apps/overview.md)
- **Google Apps Script** — [Apps Script Development](https://bkper.com/docs/platform/google-workspace/apps-script.md)

## Base URL

```
https://api.bkper.app
```

All API calls use this endpoint:

- `https://api.bkper.app/v5/books` — List books
- `https://api.bkper.app/v5/books/{bookId}` — Get a specific book

## Specifications

The API publishes an [OpenAPI](https://swagger.io/resources/open-api/) specification:

- [Bkper REST OpenAPI specification](https://bkper.com/docs/api/rest/openapi.json)

You can use this specification to generate client libraries with tools like [OpenAPI Generator](https://openapi-generator.tech/) in the language of your choice.

For **TypeScript**, we maintain an updated type definitions package:

- [`@bkper/bkper-api-types`](https://www.npmjs.com/package/@bkper/bkper-api-types)

## Authentication

Every request must include a valid OAuth2 access token in the `Authorization` header:

```
Authorization: Bearer YOUR_ACCESS_TOKEN
```

No API key is required to authenticate — the Bkper API proxy provides a managed key with shared quota.

### Obtaining a token

For local development and scripts, the easiest path is through the [Bkper CLI](https://bkper.com/docs/platform/tools/cli.md):

```bash
bkper auth login
bkper auth token
```

For unattended environments (CI, servers), provide your own token source using the OAuth2 flow that matches your setup. See [CLI → Authenticating scripts and local development](https://bkper.com/docs/platform/tools/cli.md#authenticating-scripts-and-local-development) for the canonical pattern used by the `bkper-js` SDK.

## Direct HTTP calls

Send JSON payloads with `Content-Type: application/json`.

### List books

```bash
curl -s https://api.bkper.app/v5/books \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

### Create a transaction

```bash
curl -s https://api.bkper.app/v5/books/BOOK_ID/transactions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "2025-01-15",
    "amount": "100.50",
    "creditAccount": {"name": "Bank Account"},
    "debitAccount": {"name": "Office Supplies"},
    "description": "Printer paper",
    "properties": {"invoice": "INV-001"}
  }'
```

### Using fetch (browser or Node.js)

```js
const response = await fetch('https://api.bkper.app/v5/books/BOOK_ID/transactions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    date: '2025-01-15',
    amount: '100.50',
    creditAccount: { name: 'Bank Account' },
    debitAccount: { name: 'Office Supplies' },
    description: 'Printer paper',
    properties: { invoice: 'INV-001' },
  }),
});

const transaction = await response.json();
```

### Using Python requests

```python
import requests

response = requests.post(
    "https://api.bkper.app/v5/books/BOOK_ID/transactions",
    headers={"Authorization": "Bearer YOUR_ACCESS_TOKEN"},
    json={
        "date": "2025-01-15",
        "amount": "100.50",
        "creditAccount": {"name": "Bank Account"},
        "debitAccount": {"name": "Office Supplies"},
        "description": "Printer paper",
        "properties": {"invoice": "INV-001"},
    },
)

transaction = response.json()
```

## Custom API key

For dedicated quota and project-level usage tracking, you can optionally configure your own API key:

1. Join [bkper@googlegroups.com](https://groups.google.com/g/bkper) to unlock access to enable the API on your project
2. [Create a new GCP project](https://console.cloud.google.com/projectcreate), or select an existing one
3. [Enable the Bkper API](https://console.cloud.google.com/apis/library/app.bkper.com) in the Google Cloud Console
4. [Create an API key](https://console.cloud.google.com/apis/credentials/key)
   - [Add API Restrictions](https://cloud.google.com/docs/authentication/api-keys#adding_api_restrictions) to `app.bkper.com` API only

Send your API key in the `bkper-api-key` HTTP header:

```
bkper-api-key: YOUR_API_KEY
```

> **Note**
> API keys are for project identification and quota management only, not for authentication. Do not store API keys in your code. See [securing an API key](https://cloud.google.com/docs/authentication/api-keys#securing_an_api_key) best practices.
> **Tip**
> For Google Apps Script, you can store the API key in the [Script Properties](https://developers.google.com/apps-script/reference/properties/properties-service#getScriptProperties()). To store it, open the online editor, *File > Project properties > Script properties*.
### Metrics

With your own API key, you can view detailed [metrics on the GCP Console](https://console.cloud.google.com/apis/api/app.bkper.com/metrics) for your project's API calls:

![REST API Metrics](https://bkper.com/docs/_astro/bkper-rest-api-metrics.DHXQ7CRF.png)

The [metrics dashboard](https://console.cloud.google.com/apis/api/app.bkper.com/metrics) provides information about endpoint calls, latency, and errors — a good overview of your integration's health.

### Quota

The [quotas dashboard](https://console.cloud.google.com/apis/api/app.bkper.com/quotas) provides details of the current default and quota exceeded errors.

The default shared quota is **60 requests per minute**. If you need higher limits with your own API key, please get in touch so we can discuss your case.

---
source: /docs/platform/tools/cli.md

# Bkper CLI

The official Bkper CLI is published on npm as [`bkper`](https://www.npmjs.com/package/bkper). It is the command-line interface for everything you build on Bkper and serves two roles:

- **Data management** — Work with books, accounts, transactions, and balances from the terminal
- **App development** — Initialize, clone, develop, build, sync, and deploy Bkper apps

## Installation

```bash
npm install -g bkper
```

Or run it without a global installation:

```bash
npx bkper
```

The package and executable are both named `bkper`.

## Authentication

```bash
bkper auth login   # authenticate via Google OAuth
bkper auth logout  # revoke the stored refresh token and clear local credentials
bkper auth token   # print the current access token (requires prior login)
```

`bkper auth login` authenticates via Google OAuth and stores credentials locally. The same credentials are used by:
- All CLI commands
- The `getOAuthToken()` function in scripts
- The `bkper app dev` local development server

`bkper auth token` is useful for direct API calls — pipe the output into a variable:

```bash
TOKEN=$(bkper auth token)
```

### App lifecycle

```bash
# Create a new app from the template
bkper app init my-app
cd my-app

# Or clone an existing Bkper-managed app source repository
bkper app clone <appId>

# Start worker runtime (Miniflare + tunnel + file watching)
bkper app dev

# Build the server Worker bundle
bkper app build

# Sync app metadata and, for managed apps, committed source
bkper app sync

# Deploy to the Bkper Platform
bkper app deploy

# Remove app from the Bkper Platform
bkper app undeploy

# Check deployment status
bkper app status
```

> **Note:** The project template composes the full workflow via `npm run dev` (runs Vite + `bkper app dev` concurrently) and `npm run build` (runs `vite build` + `bkper app build`). Use the template scripts for the complete development experience.

`bkper app clone` is available to authorized developers of apps with Bkper-managed source. Clone external-source apps from their Git provider instead.

Source synchronization and deployment are separate. Neither `git push` nor `bkper app sync` deploys an app. `bkper app deploy` uploads an existing local build and does not run the build itself.

See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for the managed-source collaboration workflow and eligibility rules.

### Secrets management

```bash
# Set a secret for production
bkper app secrets put EXTERNAL_SERVICE_TOKEN

# Set a secret for preview environment
bkper app secrets put EXTERNAL_SERVICE_TOKEN --preview

# List secrets
bkper app secrets list

# Delete a secret
bkper app secrets delete EXTERNAL_SERVICE_TOKEN
```

### App installation

```bash
# Install app on a book
bkper app install <appId> -b <bookId>

# Uninstall app from a book
bkper app uninstall <appId> -b <bookId>
```

### Authenticating scripts and local development

For Node.js scripts, automations, and local app development, use the CLI's stored credentials via `getOAuthToken()`:

```ts
import { Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});
```

This is the canonical pattern. The CLI handles the OAuth flow, token storage, and refresh. Do not implement custom OAuth for scripts.

## Data management commands

The CLI provides full data management capabilities:

```bash
# Books
bkper book list
bkper book get <bookId>
bkper book create --name "My Company"

# Accounts
bkper account list -b <bookId>
bkper account create -b <bookId> --name "Sales" --type INCOMING

# Transactions
bkper transaction list -b <bookId> -q "account:Sales after:2025-01-01"
bkper transaction create -b <bookId> --description "Office supplies 123.78"

# Balances
bkper balance list -b <bookId> -q "on:2025-12-31"
```

All data commands use `-b, --book <bookId>` to specify the book context.

## Query semantics (transactions and balances)

Use the same query language across Bkper web app, CLI, and Google Sheets integrations.

- `on:` supports different granularities:
  - `on:2025` → full year
  - `on:2025-01` → full month
  - `on:2025-01-31` → specific day
- `after:` is **inclusive** and `before:` is **exclusive**.
  - Full year 2025: `after:2025-01-01 before:2026-01-01`
- For point-in-time statements (typically permanent accounts: `ASSET`, `LIABILITY`), prefer `on:` or `before:`.
- For activity statements over a period (typically non-permanent accounts: `INCOMING`, `OUTGOING`), prefer `after:` + `before:`.
- For statement-level analysis, prefer report root groups (for example `group:'Balance Sheet'` or `group:'Profit & Loss'`) over isolated child groups.

```bash
# Transactions in full year 2025
bkper transaction list -b <bookId> -q "on:2025"

# Transactions in January 2025
bkper transaction list -b <bookId> -q "on:2025-01"

# Balance Sheet snapshot (point-in-time)
bkper balance list -b <bookId> -q "group:'Balance Sheet' before:2026-01-01"

# P&L activity over 2025
bkper balance list -b <bookId> -q "group:'Profit & Loss' after:2025-01-01 before:2026-01-01"
```

## Output formats

The CLI supports multiple output formats for scripting and piping:

```bash
# Table (default, human-readable)
bkper book list

# JSON (for programmatic use)
bkper book list --format json

# CSV (for spreadsheets and data tools)
bkper transaction list -b <bookId> --format csv
```

See [CLI Scripting & Piping](https://bkper.com/docs/platform/scripts/cli-pipelines.md) for scripting patterns.

## Full reference

Run `bkper --help` or `bkper <command> --help` for built-in documentation on any command.

The complete CLI documentation, including all commands and options, is available on the [bkper-cli app page](https://bkper.com/apps/bkper-cli.md).

---
source: /docs/platform/tools/libraries.md

# Libraries & SDKs

Choose the right library for your environment. All libraries are built on the [REST API](https://bkper.com/docs/platform/scripts/rest-api.md) and are used by the Bkper team to build our own products.

## bkper-js

**JavaScript/TypeScript SDK for Node.js and browsers.**

The primary client library for programmatic access to Bkper. Use it for [scripts](https://bkper.com/docs/platform/scripts/node-scripts.md), [platform apps](https://bkper.com/docs/platform/apps/overview.md), and web applications.

```ts
import { Bkper } from 'bkper-js';
import { getOAuthToken } from 'bkper';

Bkper.setConfig({
    oauthTokenProvider: async () => getOAuthToken(),
});

const bkper = new Bkper();
const books = await bkper.getBooks();
```

- [npm package](https://www.npmjs.com/package/bkper-js)
- [API Reference](https://bkper.com/docs/api/bkper-js.md)

## bkper-gs

**Google Apps Script library.**

Access Bkper from Apps Script — Google Sheets automations, triggers, add-ons. Authentication is handled by the Apps Script runtime.

```js
function listBooks() {
    var books = BkperApp.getBooks();
    books.forEach(function (book) {
        Logger.log(book.getName());
    });
}
```

- [GitHub](https://github.com/bkper/bkper-gs)
- [API Reference](https://bkper.com/docs/api/bkper-gs.md)

## @bkper/web-auth

**Web authentication SDK for the Bkper Platform.**

Browser-based OAuth for apps hosted on `*.bkper.app` subdomains. Use with bkper-js when building platform apps.

```ts
import { BkperAuth } from '@bkper/web-auth';

const auth = new BkperAuth({ onLoginSuccess: () => initApp() });
await auth.init();

// Use with bkper-js
const token = await auth.getAccessToken();
```

- [npm package](https://www.npmjs.com/package/@bkper/web-auth)
- [API Reference](https://bkper.com/docs/api/bkper-web-auth.md)

## @bkper/web-design

**CSS design tokens for Bkper web applications.**

Provides typography, spacing, border, and color tokens as CSS custom properties. Includes light/dark theme support and account-type color families. Works standalone or integrates with [Web Awesome](https://www.webawesome.com/) — if Web Awesome is loaded, Bkper tokens automatically inherit its design system values.

```css
@import '@bkper/web-design';
```

Then use the tokens in your styles:

```css
.my-component {
    font-family: var(--bkper-font-family);
    padding: var(--bkper-spacing-medium);
    color: var(--bkper-color-text);
    border: var(--bkper-border);
}
```

- [npm package](https://www.npmjs.com/package/@bkper/web-design)
- [Token Reference](https://bkper.com/docs/api/bkper-web-design.md)

## @bkper/bkper-api-types

**TypeScript type definitions for the REST API.**

Add autocomplete and contextual documentation to any TypeScript project that works with Bkper API payloads.

```bash
npm install @bkper/bkper-api-types
```

Configure `tsconfig.json` to make the `bkper` namespace globally available:

```json
{
    "compilerOptions": {
        "types": ["@bkper/bkper-api-types"]
    }
}
```

Then use the `bkper` namespace directly — no import needed:

```ts
const event: bkper.Event = await c.req.json();
if (!event.book) {
    throw new Error('Missing book in event payload');
}
const book: bkper.Book = event.book;
```

- [npm package](https://www.npmjs.com/package/@bkper/bkper-api-types)
- [API Reference](https://bkper.com/docs/api/bkper-api-types.md)

## Which library to use

| Scenario                                       | Library                                                            |
| ---------------------------------------------- | ------------------------------------------------------------------ |
| Node.js scripts and automations                | bkper-js + CLI                                                     |
| Browser (any domain, with access token)        | [bkper-js via CDN](https://github.com/bkper/bkper-js#cdn--browser) |
| Platform apps (server-side)                    | bkper-js with platform outbound auth; call `/api/*` with `Authorization: Bearer <token>` |
| Platform apps (client-side, \*.bkper.app only) | bkper-js + @bkper/web-auth                                         |
| Platform apps (styling)                        | @bkper/web-design                                                  |
| Google Apps Script                             | bkper-gs                                                           |
| Google Sheets add-ons                          | bkper-gs                                                           |
| Any language (direct HTTP)                     | [REST API](https://bkper.com/docs/platform/scripts/rest-api.md) + @bkper/bkper-api-types  |
