Add Bkper AI to an App
Ask a decision model or a language model, or run an agent, from a Bkper app Worker or your own code, and keep model output safe to use.
Bkper Platform apps can call Bkper AI models without storing a model-provider key. The platform authorizes each request as the current user and attributes usage to the app. Scripts and servers outside the platform use the same code with their own Bkper token.
Model output is a suggestion, not permission to change a Book. Keep decisions about resource movements, and any resulting writes, in application code.
Choose the kind of response
- A yes/no, a choice, or a level on a scale? Ask a decision model, such as
jev, with TypeSafe’s SDK,@typesafe-ai/sdk. - Text or a custom JSON object? Ask a language model (LLM), such as
gpt-luna, with AI SDK’s Open Responses provider,@ai-sdk/open-responses. - A conversation that works on Books? Reading and comparing Books, computing with scripts, delivering files, remembering earlier messages: run an agent with the Managed Agent API instead of calling a model.
By default, add the SDK to the Worker package. Keep the call in a server service used by your authenticated app API route, and define the route’s request and response in the app’s Zod/OpenAPI contract. For inference used only by the UI, the client can also call Bkper AI directly.
Keep authentication in the platform
For an interactive app, the flow is:
- The client calls a typed app
/api/*route throughauth.authenticatedFetch()or another authenticated client. - Bkper verifies the user’s token, removes it before invoking the Worker, and establishes user and app context for outbound requests.
- The Worker calls Bkper AI. Platform outbound adds authorization and app attribution. Do not read, forward, or store the user’s token in the Worker.
An authenticated /events handler has the same outbound context. A page request does not, so the page handler cannot call Bkper AI.
Call from the browser
The client can call Bkper AI directly with auth.authenticatedFetch(), which sends the signed-in user’s token. Choose the path by what the inference needs:
| App API route | Browser | |
|---|---|---|
| Usage attribution | The app, set by platform outbound | The bkper-ai-source header the client sends |
| Prompts, questions, and criteria | Private to the Worker | Visible to the user |
| Reuse by scripts, agents, or events | Yes, through the route | No |
| Request path | Client → app Worker → Bkper AI | Client → Bkper AI |
In both cases usage counts against the user’s allowance. See Call from a browser for a request example.
Ask a decision model
import { score, TypeSafeClient } from '@typesafe-ai/sdk';
const decisionClient = new TypeSafeClient({ apiKey: 'bkper-platform-outbound', baseURL: 'https://ai.bkper.app', defaultModel: 'jev',});
export async function scoreRecurring(description: string) { const { answers } = await decisionClient.systemOne({ state: { description }, questions: { recurring: score('How likely is this a recurring charge?', [ 'Unlikely', 'Possible', 'Likely', ]), }, }); return answers.recurring;}apiKeyis required by the SDK. In a Platform Worker, pass any placeholder: platform outbound replaces it with the user’s authorization.baseURLhas no/v1. The SDK callsPOST /v1/systemone, which behaves exactly likePOST /v1/decisions.- The answer is typed from the question. Bkper AI validates every answer against its question before responding, so your code can use it directly.
Decision Models covers designing the questions, facts, and thresholds; the AI Gateway API covers the request, answers, errors, and caching. The open-source Merge Duplicates app uses this setup to suggest duplicate pairs for human review.
Generate a language response
import { createOpenResponses } from '@ai-sdk/open-responses';import { generateText, Output } from 'ai';import { z } from 'zod';
const llmClient = createOpenResponses({ name: 'bkper-ai', url: 'https://ai.bkper.app/v1/responses',});
export async function draftReviewNote(description: string) { const { output } = await generateText({ model: llmClient('gpt-luna'), system: 'Draft a short, neutral review note. Do not invent facts.', prompt: description, output: Output.object({ schema: z.object({ note: z.string() }) }), }); return output;}- No
apiKey. The provider then sends noAuthorizationheader, and platform outbound adds it. urlis the full/v1/responsesURL.outputis parsed and validated against your schema by AI SDK. The provider requests strict structured output, which every Bkper language model supports.
Keep schemas simple. Constraints such as a string’s minimum or maximum length may not be supported by every model’s strict JSON Schema subset; check them in code after generation.
Run an agent
For a chat that does real work on Books, start a session on the Managed Agent API from the app’s server. Bkper runs the agent in the user’s private cloud workspace, with the bkper CLI, files, and durable sessions. Your app keeps its screens, prompts, and instructions. The Managed Agent is in beta.
// No Authorization header: the platform adds the user's.const response = await fetch('https://agent.bkper.app/v1/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', // Reuse the same key if you retry this request. 'Idempotency-Key': inputId, }, body: JSON.stringify({ instructions: 'You help the finance team close each month.', input: { content: [{ type: 'text', text: question }], bookContext: { bookId, query }, }, }),});const session = await response.json();Then follow the session’s stream, or poll the input until it completes, as in the quick start.
- The user’s agent. Sessions belong to the user, so work started in your app continues in Bkper Agent or any other app.
- Your services as tools. The agent can call your app’s API as the user. Describe the routes in your instructions, and the agent can run your checks instead of redoing them.
- Real changes. Unlike a model call, the agent can change Books with the user’s permissions. Say in your instructions when it may.
Outside a Platform app
Scripts, servers, and tools send their own Bkper token. Pass it as apiKey: both SDKs send it as a bearer token. Get a client each time you need one, so every call uses a current token:
import { createOpenResponses } from '@ai-sdk/open-responses';import { noul, TypeSafeClient } from '@typesafe-ai/sdk';import { generateText } from 'ai';import { getOAuthToken } from 'bkper';
const decisionClient = async () => new TypeSafeClient({ apiKey: await getOAuthToken(), baseURL: 'https://ai.bkper.app', defaultModel: 'jev', });
const llmClient = async () => createOpenResponses({ name: 'bkper-ai', url: 'https://ai.bkper.app/v1/responses', apiKey: await getOAuthToken(), });
const client = await decisionClient();const { answers } = await client.systemOne({ state: { description: 'NETFLIX.COM monthly plan' }, questions: { streaming: noul('Is this a streaming service?') },});
const { text } = await generateText({ model: (await llmClient())('gpt-luna'), prompt: 'Describe NETFLIX.COM monthly plan in five words.',});getOAuthToken()reads the credentials frombkper auth loginand refreshes them when needed. In your own server, use your own token provider.- Creating a client is cheap. Neither SDK makes a request until you ask a question.
- Label your usage with a
bkper-ai-sourceheader, such asmy-script:defaultHeadersin the TypeSafe SDK,headersin Open Responses. In a Platform app, outbound sets the source to the app.
Handle errors
Every Bkper AI error has the same envelope: { error: { message, type, param, code } }. Branch on error.code, not only on the HTTP status: a 429 can mean an exhausted allowance or a throttled provider.
- TypeSafe SDK: an
APIErrorwithstatusand the envelope inbody, so readerror.body.error.code. Network failures and timeouts throwAPIConnectionError. - AI SDK: an
APICallErrorwithstatusCodeand the envelope indata, so readerror.data.error.code. Output that does not match your schema throwsNoObjectGeneratedError.
Both SDKs retry rate limits and server errors twice by default. A failed attempt costs nothing: Bkper AI charges only successful answers.
Map these errors to your route’s typed error response. Keep the code and message so users understand failures such as an exhausted allowance, but never return prompts, raw responses, or stack traces.
SDK notes
- Passing your own
fetch. The TypeSafe SDK callsfetchas its own method, which the Workers runtime rejects withIllegal invocation. Wrap it:fetch: (input, init) => myFetch(input, init). Without the option, the SDK’s default works. - No
nullvalues. The TypeSafe SDK’s types acceptnullfor state, instructions, and some criteria. Bkper AI rejects them with400. - No
client.models.list(). It expects TypeSafe’s catalog shape. Use theGET /v1/modelscatalog. - Other System One clients take
https://ai.bkper.app/v1as their base URL. - Flexible JSON schemas. Open Responses always requests
strict: true. For a schema that needsstrict: false, such as a typed dynamic map, use@ai-sdk/openaiwith base URLhttps://ai.bkper.app/v1, its.responses()model, andstrictJsonSchema: false.
Before using the result
- Send only task-relevant data. Validate input and Book permissions at the app API boundary.
- Keep writes deterministic. An answer or generated text must not create, merge, or alter a Transaction without application rules and any required human confirmation.
- Test the service. Mock
fetchand check what is sent, that the Worker adds no user token, and that errors stay actionable. Then run the app’s normal check and build.
Next steps
- Decision Models: questions, answers, thresholds, and caching.
- Models and Usage: models, usage rates, the allowance, and privacy.
- AI Gateway API: tokens, raw HTTP requests, capabilities, and errors.
- Managed Agent API: sessions, instructions, files, and streams.