# Set up barua with your coding agent

barua sends email from the facts your product's server reports, such as a signup or a paid order. Your coding agent can set it up and maintain it through the barua-setup skill. You choose its permissions and the changes you want to review. This page is the Markdown twin of https://barua.ai/docs/install.

## Connect your agent

barua is in early access, so your workspace starts with a conversation (see Getting access, at the end of this page). The workspace owner can then create an agent connection in Settings → Agents or through the owner API. Choose what it can change and whether you want to review the plan, brand, plays or launch. You can delegate every permitted step or keep any of those checkpoints. The same choice applies to later updates.

- The management key (`BARUA_MANAGEMENT_KEY`) lets your agent carry out its grant. Keep it in the agent's secret store. The helper can transfer newly created keys directly to an authorized destination without printing them.
- Event keys are separate. The test key goes in development as `BARUA_API_KEY`, in a git-ignored file that isn't a symlink. The live key goes into production secrets through your already authorized hosting tool. A test key checks events without creating people or sending email.

You can change or revoke an agent connection. Revoking it leaves deployed event keys working; pause sending or revoke those keys separately when needed. History imports require an explicit grant. Customer reads and erasure each need their own permission. With only a test event key, the skill still writes and checks your emitters and hands the remaining steps to you. The [management reference](https://barua.ai/skills/barua-setup/references/management.md) documents the headless path.

## Hand one prompt to your agent

The Setup screen has this prompt ready to copy. Give it to your coding agent with your project open, and name any other repos it should read. Claude Code, Codex, Cursor and any other agent that reads a SKILL.md file can run it.

```text
Fetch and follow the instructions at https://barua.ai/setup.md to set up barua for this project.
```

The [setup guide](https://barua.ai/setup.md) tells your agent how to inspect the project and resume existing setup. It loads the detailed skill with `npx skills add getbarua/skills --skill barua-setup`. To install it for one agent without questions, add `-a claude-code` or `-a codex` and `-y` to that command. The skill is public at https://github.com/getbarua/skills, and your agent can also read it at https://barua.ai/skills/barua-setup/SKILL.md.

## What your agent does

The skill gives your agent these steps and rules, and your agent follows them in your repos, on your machine:

- It reads the repos you named and finds the line in your server code where each fact becomes true, such as the commit that saves a new user. It never fakes an event your product doesn't have.
- It proposes the full event plan, even when the core events are all you need. It approves the exact version when delegated, or keeps an operation waiting for your review.
- It writes the code that sends each event from your server once the database commit is done. It never puts that code in a browser.
- It runs each flow through your product's own code, such as its test suite. Then it checks the events with the skill's verify script until every one passes, or until a step needs you.
- It derives brand details and a business description from the repos. With management access it can confirm the initial brand and make requested later changes. It asks for facts the repos do not establish.
- If your product deletes accounts, it adds a job that erases the person from barua too.
- It adds a short block to your repo's agent instructions (`AGENTS.md`, which Claude Code reads through `CLAUDE.md`), so the next agent keeps the events right. It never writes through a symlink. When one of those files links somewhere else, it gives you the block to add there by hand.

The skill tells your agent never to send barua your source code or environment secrets. It sends the declared events and specific configuration fields, including the brand and business description. It keeps a source for the product facts it uses.

When your frontend does something your server never hears about, your agent reports it as `needs_server_call` with a suggested endpoint. Whether to add that endpoint is your call.

## The core events

barua defines the same core events for every product, as `core@1`. Your server sends each one when its fact becomes true, with your own user id.

<!-- docs-check:core-events -->
- Required: `signed_up`
- Declarable: `activated` `created` `purchased` `churned`
- Optional: `consent_updated` `checkout_started` `order_shipped` `order_delivered` `order_delayed` `order_cancelled` `order_refunded` `trial_started` `trial_ending` `trial_ended` `subscription_started` `subscription_cancelled` `signed_in`
<!-- /docs-check:core-events -->

`signed_up` is required. When your product has no source for a declarable event, such as `purchased` in a product that takes no payments, your tracking plan declares it `not_emitted` with a reason, and plays that need it stay off. Send an optional event whenever your product has its moment. Receipts and order updates come from `purchased` and the order events.

When your server knows someone's time zone, send it on `signed_up` as `timezone`, such as `Europe/Oslo`. Without the pixel, that's the only place barua can learn it, and their marketing email otherwise waits for the send window in your workspace's time zone, or in UTC until you set one under Settings.

Facts only your product has, such as a project going live, become custom events in your tracking plan, and barua accepts them once you approve the plan. Every property of every core event is in the full contract at https://barua.ai/docs/events.md, and `GET /api/integrations/v1/plan` returns your workspace's approved custom events with theirs.

## Choose what to review

Every play needs an exact approval before it can send. You choose whether your agent can give it or whether it waits for you. Settings, Agents shows pending requests and their content. Approval applies to that action and version; a changed request needs a new decision. The API supports the same review without a browser.

- The tracking plan defines your custom events and which core events your product does not emit.
- The brand includes your business description. Once it is confirmed and the plan approved, barua starts writing the emails. Forwarding an existing email is optional and supplies its design.
- Your sending domain needs verified DNS records. The agent can add them with your authorized DNS tool and check them through Barua. DNS changes can take a while, so start early.
- Each play combines an event rule and its emails. You or your authorized agent inspect real renders and approve the exact languages. Marketing plays still follow the consent rule in the [privacy page](https://barua.ai/privacy).

Receipts and order updates work the same way. barua writes their words once in your brand's voice. When it can't, the email uses barua's own fixed wording in English or Norwegian. The Plays map shows which one you're approving. A reader in a language you haven't approved gets the English email when English is approved, and otherwise your first approved language. Either way you approve them before any goes out. They skip the marketing consent check, because they are about an order the person placed. The order details in them, such as the total, come from the event your server sent.

## Go live

Enable live intake through an authorized management connection. If you chose launch review, the request waits in Settings → Agents. Turn play sending on in Setup or through the same connection. A new workspace starts with sending off; approving templates does not turn it on. All current owners need current terms before live intake can start. Activate sending before the occurrence used for proof, because an event from before play emails are first on never starts a play.

Then install the live key in production secrets and deploy. Start a fresh delivery proof for that key and an approved play revision, then trigger a new qualifying action through your live product. Barua follows its event through the play and provider delivery report. The agent reports whether it is waiting on a sending window or another condition. Provider acceptance alone does not establish delivery. Old setup stamps and duplicate events don't count either. The legacy verify script still checks live event intake; it does not replace this proof.

From then on, barua sends when a real person matches a play you approved and, for a marketing play, the consent rule lets it reach them. On a quiet launch that can take a while.

## The pixel (optional)

Plays run on your server's events, so you can start without the tag. On your site, it builds a profile from what each visitor did, read from the markup your pages already publish. The rest of this page covers the tag and the calls it exposes.

## The tag

```html
<script async src="https://barua.ai/b.js" data-key="pk_…"></script>
```

Your key is on the Setup screen in the app, shown once when it's issued. Paste the tag high in the `<head>` on every page. Capture starts immediately, and the first profiles fill within a couple of hours.

## The key is public by design

The key ships in your HTML, so treat it as public; it's built for that. All it can do is post visitor activity into your workspace; it can never read anything back. Browser posts are checked against your site's origin once it's set in the app (a best-effort abuse check, never an authentication boundary), and the key is stored hashed on our side.

Want a fresh one? Rotate it from Setup in one click. The old key is refused everywhere within a few minutes, and that includes the copy still sitting in your deployed HTML: paste the new key into the tag and redeploy right after rotating, or capture stops until you do.

## What it captures

A masked replay of each visit, plus a stream of plain events read from the markup your site already publishes. Masking happens in the browser: every input value and every piece of rendered text is replaced before anything is sent, with one narrow stylesheet edge described on the privacy page. An iframe's contents are never recorded, and an element you mark with `data-cg-block` keeps only an empty placeholder in the replay. The full stance is on the [privacy page](https://barua.ai/privacy).

The events:

<!-- docs-check:events -->
- Pages: `pageview` `page_title` `page_leave` `tab_visible` `attention`
- Interaction: `click` `rage_click` `exit_intent` `form_abandon` `form_state` `site_search` `custom`
- Commerce: `product_view` `add_to_cart_click` `cart_state` `datalayer`
- Context: `attribution` `device` `geo` `js_error`
- Identity: `identify`
<!-- /docs-check:events -->

One detail worth knowing: on a shop whose own cart endpoint doesn't answer, barua infers the cart from add-to-cart clicks and marks it as inferred, so downstream decisions know the difference.

Cart events distinguish a confirmed cart from an estimate or an unavailable reading. A confirmed empty cart clears the previous contents. Product details can refresh when your page updates its structured markup.

Form events distinguish an attempt from a confirmed completion. They include field types and browser validation categories. A submitted form with an unknown result stays unresolved. Clicks include the destination link when one exists, and attention is saved when a tab moves into the background.

## Tag attributes

<!-- docs-check:attributes -->
| Attribute | What it does |
| --- | --- |
| `data-key` | Required. Your workspace key: `pk_` followed by 48 hex characters. |
| `data-consent` | Set to `"required"` to capture nothing, and write nothing to the device, until your consent banner calls `barua.consent(true)`. A refusal stores only the refusal marker. The mode for an EU posture. |
| `data-auto-identify` | Set to `"off"` to turn off automatic email pickup. Explicit `barua.identify()` calls still work. |
| `data-endpoint` | Overrides where event batches post, for proxying through your own domain. The handed-out snippet doesn't need it, and the once-per-session city lookup still calls barua.ai directly. |
| `data-cg-block` | This one goes on any element of your page. Its contents are never recorded; the replay keeps an empty placeholder carrying the element's tag, class, width, and height. |
<!-- /docs-check:attributes -->

## Serve it from your own domain

The tag works without this section: as handed out, it captures today, and as of August 2026 the common blockers' filter lists do not carry barua, so their domain rules have nothing to match. This section is for the day some list adds it. Most ad blockers block by domain. Serve the tag and its events from your own domain and there is no third-party domain left for a filter list to match. Consent is a separate promise, and the proxy changes nothing about it. The rewrite routes live in your hosting configuration, with no DNS changes. Your sending domain setup stays exactly as it is. The one exception is the Cloudflare Worker path, which assumes your site already runs through Cloudflare; if it does not, use the rewrite route your own host provides instead. The script itself sends event batches with credentials omitted, leave-time flushes included, so proxied batches carry no site cookies on any host; the Worker and Nginx recipes also strip the header outright. What remains on plain-rewrite hosts is the script file request, plus the rare leave-time flush too large for a credential-free send, whose cookies travel in transit only: barua's event intake ignores cookies and stores none.

Your setup page shows the routes ready to paste. Netlify and Vercel take two rewrite lines each. Cloudflare takes a small Worker on the same two paths, and any proxy that can forward two paths works the same way: one serves the script from a path on your site, the other forwards event batches. The tag then uses `data-endpoint` with that path. Pick short, bland paths. Words like analytics, stats, track, or pixel get matched by filter patterns, and blockers running in stricter modes can still match paths, which is exactly why the names matter. The proxy changes nothing about consent: the script still captures nothing before `barua.consent(true)`.

One optional line of server middleware goes further: when a request URL carries the `b=` email token, report it, and the click is captured even in a browser where the script never ran. The exact call is on your setup page. This runs in your server, outside the script's consent gate, and it stores one scrubbed URL of a barua-tagged click. The row names no person and no session, and the token in the address links it to the email it came from. If your consent posture requires it, fire it only when your own consent cookie grants. It is your middleware, and that one line is yours to gate.

## The JavaScript API

The tag exposes `window.barua` with four calls.

<!-- docs-check:api -->
### barua.identify(email, externalId?, traits?)

Usually you never call it: when a visitor gives your site their email through a popup or a checkout, barua picks it up. For a logged-in app, call it when each page restores a signed-in session or changes account:

```js
barua.identify('them@theiremail.com')
```

The sign-in event itself lands after the async tag has loaded in practice. A call that can run at page boot instead (a restored session) gets the same guard as consent: `window.barua?.identify(…)`, repeated when your session state settles. On a consent-required site, an identify fired before the visitor opts in is held in memory and delivered when `barua.consent(true)` arrives.

`traits` carries what no form captured, an OAuth signup's name for instance. Recognized keys: `first_name`, `last_name`, `name`, `phone`. Values are capped at 80 characters, and any other key is dropped.

`externalId` is your own user id. barua keeps it as unverified evidence and never trusts it to say who someone is: a browser can claim to be anyone. To make an id stick, send it from your server as the user id on a `signed_up` sent with your live key. Create that secret integration key in Setup or through an authorized management connection (see Connect your agent). Once that signup applies, your later events with the same id reuse it. Enable live intake through an authorized management connection after setup checks pass (see Go live).

A different identity starts a fresh browser context. Activity already queued keeps its original owner. A SHA-256 marker allows the tag to notice identity changes across page loads without storing the email address in that marker. A page that has identified someone keeps that account until its own next `identify` or `reset` call. Unidentified tabs can adopt the latest shared browser context. If the browser cannot coordinate the first identity claim safely, earlier anonymous activity may remain separate from the identified profile.

### barua.reset()

Call this when the page learns that someone has signed out. It starts fresh anonymous and session IDs. Other unidentified tabs on the same site adopt the change; pages that identified their own account follow their own sign-in state. Already queued activity keeps its original IDs. Use `barua.consent(false)` to stop collection and clear pending activity.

### barua.track(name, properties?)

Use this when your app knows a meaningful action happened. For example: `barua.track('feature_used', { content_type: 'editor', count: 1 })`. Recognized properties accept short codes or numbers; unknown properties are dropped. Do not pass form answers or free text. Call once for each action. The tag reuses its generated event ID when it retries delivery.

Supported short-code fields are `form_id`, `attempt_id`, `step`, `step_id`, `status`, `result`, `method`, `plan`, `plan_id`, `content_id`, `content_type`, `item_id`, and `variant_id`. Use the same `step_id` and item ID when different event names describe one operation's progress and outcome. Supported numeric fields are `step_index`, `count`, `duration_ms`, and `value`; `currency` accepts a three-letter code.

### barua.consent(ok)

For sites running `data-consent="required"`. Wire it to your consent banner: `barua.consent(true)` starts capture, and `barua.consent(false)` stops the recorder, clearing both the queue and the pixel's stored values; the only thing a refusal keeps is the refusal marker itself. Calling it repeatedly is safe.

The pixel loads async, so a banner that can fire before it's ready should guard the call (`window.barua?.consent(true)`) and repeat it on the consent tool's change event. Once a choice is stored, later pages need no call at all: the pixel reads it itself on load.
<!-- /docs-check:api -->

## If your visitors are in Europe, read this one

By default the tag captures from the first pageview and writes its identifiers to the browser's localStorage. Unconfirmed activity batches expire after 24 hours, within a 1 MiB limit, and are removed after the server confirms them. Expired batches are deleted the next time the tag is allowed to run. Replay is excluded from this saved queue. That default is right for plenty of sites and wrong for some, and which one you are is a legal question rather than a technical one.

Across the EEA and the UK, putting anything on a visitor's device or reading it back needs their consent first, unless it is strictly necessary for something they asked for. Local storage counts, the same as a cookie: European regulators settled that question explicitly, and the fact that barua sets no cookies does not exempt it. So if your visitors are there, run consent mode:

```html
<script async src="https://barua.ai/b.js" data-key="pk_…" data-consent="required"></script>
```

Nothing is captured and nothing is written to the device until your banner calls `barua.consent(true)`. Wire that call to whatever consent tool you already run, and call `barua.consent(false)` when someone withdraws, which clears the stored values and keeps only the refusal.

The second switch is about contact details. barua picks up an email or a name at the moment a form or a popup actually sends, by default. That is a real collection made by you, and your own notice has to cover it. If it does not, or if you would rather hand identities over from your own code, turn the automatic pickup off:

```html
<script async src="https://barua.ai/b.js" data-key="pk_…" data-auto-identify="off"></script>
```

One more thing worth a minute of thought. barua masks every input value and every piece of rendered text in the browser, but it deliberately keeps the context it needs to be useful: page titles, product names and prices, and what someone searched for on your site. On most sites that is harmless. If a page title would say that a person was reading about a medical condition or a religion, that is a different category of data with a much higher bar.

**Blocking the visible element is not enough for a title.** The pixel drops a page title only when the `<title>` element itself, or something above it, carries the attribute, so a page that hides its body but leaves its title alone still sends the title. For a route like that, mark the title:

```html
<title data-cg-block>Your order</title>
```

Or keep the tag off the route entirely, which is the safer answer when a whole section of the site is sensitive. `data-cg-block` on a body element still does its own job: the contents of that element are never recorded. The [acceptable use policy](https://barua.ai/acceptable-use) is where this is spelled out, and the [privacy page](https://barua.ai/privacy) has the full detail of what is masked.

## Single-page apps

Nothing extra to do. The pixel follows `pushState` and `popstate` navigations, and corrects the page title after your router settles. Call `identify` when a page restores or changes its signed-in account, and `reset` when it learns of a sign-out.

## Getting access

Access is a conversation today. Write chris@barua.ai, and a person answers.
