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.
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.
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.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 documents the headless path.
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.
The setup guide 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.
The skill gives your agent these steps and rules, and your agent follows them in your repos, on your machine:
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.
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.
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
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.
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.
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.
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.
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.
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 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.
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.
The events:
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
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.
| 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. |
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 tag exposes window.barua with four calls.
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:
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.
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.
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.
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.
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:
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:
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, a debt, or a faith, 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:
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 is where this is spelled out, and the
privacy page has the full detail of what is masked.
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.
Access is a conversation today. Write chris@barua.ai, and a person answers.