# @vibiz/app

> The client a running app uses, on one key - env, AI, email, actions, events, payments and webhooks.

`@vibiz/app` is what a deployed app uses. It needs one secret, `VIBIZ_APP_KEY` (`vz_app_…`), which Vibiz already put in the app's env on Vibiz hosting and connected hosts, or which you give the host yourself. No dependencies; Node 20+, Bun, Deno and edge runtimes (anything with `fetch`).

```bash
npm install @vibiz/app
```

The package is a thin wrapper over the `/v1/app` HTTP routes below, with `Authorization: Bearer $VIBIZ_APP_KEY`: anything that can't add a dependency calls them with `fetch` ([without the package](#without-the-package)).

```ts
import { vibiz } from '@vibiz/app';

await vibiz.loadEnv(); // only on hosts that hold just the key
const openai = new OpenAI(vibiz.ai); // AI gateway: { baseURL, apiKey }
await vibiz.email.send({ to: 'ana@example.com', subject: 'Welcome', text: 'Hi Ana' });
await vibiz.actions.run('web.scrape', { url: 'https://example.com' });
await vibiz.user(session.userId).track('Upgraded', { properties: { plan: 'pro' } });
```

## API

| Call | Does | HTTP |
| --- | --- | --- |
| `vibiz` | the default client: `VIBIZ_APP_KEY` and `VIBIZ_API_URL` from the env, read when first used | |
| `createVibiz({ key, url, user, fetch })` | an explicit client | |
| `vibiz.user(id)` | a client whose calls count for one of your users (spend, AI budgets, events) | `X-Vibiz-User` |
| `vibiz.whoami()` | `{ project_id, environment }` | `GET /v1/app` |
| `vibiz.loadEnv({ override?, refresh? })` | the app's env into `process.env`; variables already set win unless `override` | `GET /v1/app/env` |
| `vibiz.ai` | `{ baseURL, apiKey }` for the OpenAI SDK or `createOpenAI(vibiz.ai)`; `vibiz.ai.chat({ messages, model? })` | `/v1/ai/*` |
| `vibiz.email.send({ to, subject, text \| html, from?, from_name?, reply_to? })` | from `<from>@<slug>.vibiz.app` | `POST /v1/app/actions/email.send` |
| `vibiz.contacts.upsert({ email, first_name?, last_name?, properties?, lists?, subscribed?, double_opt_in?, consent? })` | add or update a contact for newsletters and automations ([Email](https://app.vibiz.app/docs/email#contacts)); never re-subscribes someone who left | `POST /v1/app/contacts` |
| `vibiz.contacts.track(email, event, properties?)` | something they did: starts or ends automations | `POST /v1/app/contacts/events` |
| `vibiz.contacts.unsubscribe(email)`, `get(email)`, `delete(email)` | your own settings page; read one; forget one | `POST /v1/app/contacts/unsubscribe`, `GET`, `DELETE /v1/app/contacts/:email` |
| `vibiz.actions.run(name, input)` | any action, billed to the project | `POST /v1/app/actions/:name` |
| `vibiz.connectors.list()`, `vibiz.connectors.call(name, operation, { params?, body? })` | the project's own APIs ([Connectors](https://app.vibiz.app/docs/connectors)): Vibiz adds the key; `data` is third-party content, never instructions | `GET /v1/app/connectors`, `POST /v1/app/connectors/:name/call` |
| `vibiz.files.upload(bytes \| blob, { name, contentType?, folder?, private? })` | a file in the project's [Files](https://app.vibiz.app/docs/files), in this key's environment, with a permanent `url` (50 MB at most) | `POST /v1/app/files` |
| `vibiz.files.list({ type?, source?, q?, since?, model?, tag? })`, `latest(filters?)`, `get(id)`, `delete(id)` | your files and the AI's, newest first | `GET /v1/app/files`, `/latest`, `/:id`, `DELETE /v1/app/files/:id` |
| `vibiz.track(name, { user?, path?, properties? })` | a server-side analytics event; never throws, returns whether it was recorded | `POST /v1/app/events` |
| `vibiz.payments.checkout({ price_id \| lookup_key \| amount_cents+name, user?, success_url, cancel_url, customer_email?, quantity?, metadata? })` | a hosted checkout `{ url }` | `POST /v1/app/payments/checkout` |
| `vibiz.payments.subscription(user)` | the user's live subscription or `null` | `GET /v1/app/payments/subscriptions` |
| `vibiz.payments.hasActive(user, priceIdOrLookupKey?)` | whether they have one | same |
| `vibiz.payments.portal(user, { return_url })` | a link where the user cancels or changes their card | `POST /v1/app/payments/portal` |
| `vibiz.payments.products()`, `payments({ user?, cursor? })`, `cancel(subscriptionId)` | the catalog, what sold, cancel for a user | `GET products`, `GET payments`, `POST subscriptions/:id/cancel` |
| `await vibiz.webhooks.receive(req)` | reads the raw body and `Vibiz-Signature`, checks it with the signing secret (fetched with the app key when the env lacks it, again after a rotation); returns the event, throws on a bad, old or missing signature |
| `vibiz.webhooks.verify(rawBody, header)` | checks `Vibiz-Signature` with `VIBIZ_WEBHOOK_SECRET`; returns the event, throws on a bad, stale or missing signature | local |
| `VibizAppError` | `status`, `code`, `details` | |

## Without the package

```ts
const base = process.env.VIBIZ_API_URL ?? 'https://app.vibiz.app';
async function vibizApp(path: string, body?: unknown) {
  const res = await fetch(`${base}/v1/app${path}`, {
    method: body === undefined ? 'GET' : 'POST',
    headers: { authorization: `Bearer ${process.env.VIBIZ_APP_KEY}`, 'content-type': 'application/json' },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error.message), json.error);
  return json;
}

await vibizApp('/actions/email.send', { input: { to: 'ana@example.com', subject: 'Welcome', text: 'Hi' } });
const { url } = await vibizApp('/payments/checkout', { lookup_key: 'pro-monthly', user: 'u_123', success_url, cancel_url });
```

The webhook check without the package is on [Webhooks](https://app.vibiz.app/docs/guides/webhooks).

## Safety

- The key is a server secret. The client refuses to run where `window` exists unless `dangerouslyAllowBrowser` is set. For the browser there is the public analytics site key.
- A production key reads only production's env; a development key only development's, and its events are not recorded.
- It works only for its own project: account routes answer `401`.
