ARCY AI

Reference

Every arcy.js call, including registerHandler for an operation with no API route, every init() option, event and exported type, and the fix for every console warning.

arcy.js has one entry point, arcy.init(token), identical in every framework and on both install paths (npm and the HTML snippet). Code sets only what has to be known before the dashboard can be reached: the reply language, whether the panel opens on load, the content locale, where requests go and the headers a flow's Request step sends. The agent's persona, its Sources, its flows, its limits and its translations are all configured in the dashboard. The token is on Configure > Installation; the install itself is on the Quickstart. The ARCY MCP is a separate server for your coding agent, not part of arcy.js: see ARCY MCP.

No call throws and no promise rejects. A misuse produces a console.warn prefixed [arcy] and does nothing. Every warning is listed under Console warnings.

init(token, options?)

ts
init(token: string, options?: ArcyOptions): Promise<void>

Mounts ARCY for one environment. token is that environment's public Token. A second call on the same page, or a call with no token, is ignored with a warning.

options accepts exactly six keys. Any other key is ignored with a warning naming it.

ts
interface ArcyOptions {
  locale?: string
  defaultOpen?: boolean
  contentLocale?: string
  apiBase?: string
  requestHeaders?: () => Record<string, string> | Promise<Record<string, string>>
  listen?: { posthog?: unknown; mixpanel?: unknown; amplitude?: unknown }
}
OptionValueEffect
locale"en", "tr", "de" or "ru"The language the agent replies in by default. Any other value is ignored. Does not choose translated content.
defaultOpentrue or falseOpens the chat panel on load. Unset, the panel stays closed. Ignored for a visitor outside the chat bar audience.
contentLocale"en-US", "tr-TR", "de-DE" or "ru-RU"Which locale's translated content is served when you know the visitor's region before they identify. Not derived from locale. See Localization.
apiBaseAn origin, or an origin with a path prefix, no trailing slashWhere every request, upload and font goes instead of https://api.arcyai.com. For pages under a Content Security Policy that allow no outside origin. See Troubleshooting.
requestHeadersA function returning a plain object of headers, or a promise of oneCalled in the visitor's browser right before each Request step of a flow; the headers go with the call beside the visitor's own cookies. Never stored, never sent to ARCY. A cookie-session Product needs nothing here. See Endpoints.
listenAn object naming the PostHog, Mixpanel or Amplitude instance your code createdFor a tool installed from npm, which has no window global for ARCY to find. Each key is optional. The tool must still be switched on under Integrations, otherwise the instance is ignored with a warning. When both an instance and a global exist, the instance is used and the global is never read. Google Analytics 4 has no key here; its dataLayer is always global.
ts
import arcy from "arcy.js"

arcy.init("YOUR_ENVIRONMENT_TOKEN", { locale: "tr", contentLocale: "tr-TR" })

arcy.init("YOUR_ENVIRONMENT_TOKEN", {
  requestHeaders: () => ({ Authorization: "Bearer " + getToken() }),
})

import posthog from "posthog-js"
arcy.init("YOUR_ENVIRONMENT_TOKEN", { listen: { posthog } })

identify(userId, attributes?, options?)

ts
identify(userId: string, attributes?: AttributesInput, options?: IdentifyOptions): Promise<void>

Tells ARCY who the current user is. userId must be a non-empty string; anything else is ignored with a warning. attributes is a flat object of the built-in attributes and the keys you have declared under Configure > Attributes. Each value is a string, number, boolean, null, an array of strings, an object, or an array of objects. A value of any other shape, a Date for one, is dropped with a warning naming the key. The caps on an object are on Attributes.

Three cases stay distinct: a key you leave out is not touched, undefined is the same thing spelled in JavaScript, and null clears the stored value. That lets you pass an optional field from your auth provider straight through:

ts
arcy.identify(user.id, {
  user_email: user.primaryEmailAddress?.emailAddress,
  user_signed_up_at: user.createdAt.toISOString(),
  plan_value: 249,
  plan_cycle: "monthly",
  plan_currency: "TRY",
})

options.userHash carries the identity-verification signature your server computed. The recipe is on the Quickstart. A userHash that is not a string is ignored with a warning.

Built-in attributes

ARCY understands these keys in every Product with nothing declared. A value that fails its check is refused for that key only, the rest of the call is stored, and the key shows as refused on Attributes. So is any value, built-in or yours, that looks like a card number, a bank account number (IBAN) or an API key: ARCY never stores payment or credential data, and the refused value is not logged.

KeyTypeAccepted value
user_emailTextUp to 1,000 characters.
user_first_nameTextUp to 1,000 characters. Send the two halves, never one full name.
user_last_nameTextUp to 1,000 characters.
user_signed_up_atDatetimeISO 8601 text, such as user.createdAt.toISOString(). Stored in UTC.
organization_idTextThe account the user belongs to. Users sharing it roll up into one organization.
organization_nameTextThe account's name.
organization_signed_up_atDatetimeISO 8601 text.
plan_nameTextThe plan's name.
plan_valueNumberA number of 0 or more, never text: 249, not "249".
plan_cycleTextmonthly or annual, nothing else.
plan_currencyTextOne of USD, EUR, GBP, JPY, CNY, AUD, CAD, CHF, INR, TRY, in any case. Without it, plan_value is left out of every total across users.

ARCY fills four keys itself: user_first_seen_at, user_last_seen_at, organization_first_seen_at and organization_last_seen_at. Sending one is refused.

Your own attributes may start with user_, organization_ or plan_, such as user_role; only the fifteen names above are taken. The rules for a key are on Attributes.

locale_code is not an attribute. Sent through identify() or updateUser(), it sets the visitor's content locale for this session, the same as init()'s contentLocale, and is never stored on the user.

identifyAnonymous(attributes?, options?)

ts
identifyAnonymous(attributes?: AttributesInput, options?: IdentifyAnonymousOptions): Promise<void>

For pages with no signed-in user. Reuses or mints an id kept in a first-party cookie on your domain, so a returning visitor is one person across sessions and subdomains, and the activity before a sign-up joins the account after it. An anonymous visitor's chat answers spend the same AI credits as an identified user's; the caps are on Limits.

options.id hands arcy.js your own id for this visitor, such as a guest id, to use in place of the one it makes. Pass the value you put in the anonymous_id column of an Import, and the visitor joins the anonymous history you imported:

ts
arcy.identifyAnonymous({}, { id: guest.id })

Call it at page load, before the chat opens: an id handed over mid-conversation starts the visitor afresh. A UUID (version 1 to 8) is used as it is, lowercased. Any other id is trimmed, may be at most 256 characters, and becomes a fixed UUID, the same one the Import computes from that value. Like the id arcy.js makes, yours is stored only after the visitor's consent. A refusal or a withdrawal forgets it, so pass it on every call. An empty or longer id is ignored with a warning, and the visitor keeps the id arcy.js made.

updateUser(attributes?)

ts
updateUser(attributes?: AttributesInput): Promise<void>

Merges attributes into the current identity without changing who it is. Takes no userHash: it inherits the verification state of the identify() call before it. Called before any identify() or identifyAnonymous(), it sends nothing.

reset()

ts
reset(): void

Clears the current identity and starts a fresh anonymous id. Call it from your sign-out handler so two people on one browser are never merged. It also ends a running flow, which your flow_abandoned handler hears, and drops the offers, triggers and Checklist waiting for the person who left and any unfinished flow saved for them, so the next person starts clean.

consent(granted)

ts
consent(granted: boolean): void

Records the visitor's answer to your cookie policy. Call it with true from your banner's Accept handler and false from its Reject handler, any time after init(). false deletes every cookie and key ARCY wrote on the device. What each answer stops, and how the chat asks again, is on Consent.

consentState()

ts
consentState(): "granted" | "refused" | null

The visitor's current answer on this device. null means no answer yet, so your banner should show itself. On the HTML snippet path it also answers null until arcy.js has loaded, so read it inside on("ready").

onConsent(handler)

ts
onConsent(handler: (granted: boolean) => void): () => void

Fires on every change from any source, including an Accept inside the chat, and returns an unsubscribe function. Update Google Consent Mode, your CMP or your own flag here. Wiring examples are on Consent.

on(event, handler)

ts
on(event: "ready" | "open" | "close", handler: () => void): () => void
on(
  event: "flow_started" | "flow_completed" | "flow_abandoned",
  handler: (detail: { key: string; source: FlowStartSource }) => void,
): () => void

Subscribes to an event and returns an unsubscribe function. Callable before init(), and reset() keeps every subscription.

EventFires whenHandler receives
readyinit() has configured the session.Nothing
open, closeThe chat panel opens or closes.Nothing
flow_startedAny flow starts, whoever started it.{ key, source }
flow_completedA flow reaches its end.{ key, source }
flow_abandonedA flow stops before its end: the visitor stops it, a trigger ends it, reset() ends it, or it fails. A tab closed mid-flow sends nothing.{ key, source }

key is the flow's Key from its settings on Flows. source says what started the run:

sourceStarted by
starterA Starter Flow pill on the bar or the welcome screen
drawerThe Flows drawer in the chat
shortcutA flow offered in the chat transcript
target_suggestA visitor accepting an Agent's Suggest
target_runAn Agent's Run
checklistA Checklist item
codeYour own startFlow() call
flow_routeARCY running a flow that matched the user's question, inside its chat answer (Flows)

The names match the events on Flows, so a count in your own analytics and a count on a flow's analytics read the same thing:

ts
arcy.on("flow_completed", ({ key, source }) => {
  posthog.capture("flow_completed", { flow: key, source })
})

An event name outside these six is ignored with a warning. A handler that throws is caught with a warning; the other handlers and the flow keep running.

startFlow(key)

ts
startFlow(key: string): void

Starts the published flow with this Key, from your own button or menu. Flow settings on Flows shows the line to copy next to the key. Renaming the key stops every call made with the old one.

ts
<button onClick={() => arcy.startFlow("setup_payouts")}>Set up payouts</button>

A call with a click or key press behind it counts as the visitor starting the flow. A visitor who has not accepted your cookie policy, or refused it, gets the chat, which asks first; the flow starts on their next click after they accept. A call with neither behind it, such as one on load or inside on("ready"), is ARCY speaking first: the chat asks only a visitor who has never answered, and a visitor who refused sees nothing. The run's source is code.

A key with no flow published in this environment, or a flow whose audience does not include this visitor, starts nothing and shows nothing; the console names the key. A second call for the same key while the first is still loading, or a call for a flow already running, starts nothing new. A call made before init() resolves waits for it; a call before init() is ever called is ignored with a warning. On the HTML snippet, call it from a click or inside on("ready"); see Loading and page changes.

track(name)

ts
track(name: string): void

Reports a named event from your own code. Name only: lowercase letters, digits and underscores, at most 64 characters, starting with a letter. A name outside that shape is dropped with one console warning per name. Nothing is stored until the visitor has given consent, and a call made before init() resolves waits for it.

Declare the name first. A name no event on Events declares is dropped on arrival, counted there against its own name, and never counted against your plan's tracked events. Declaring takes one press on that page.

A declared event can be a flow's Completion event (optional), a funnel step, an Experiment's condition, a Specialist's finish line or a checklist item's Done rule.

Call it on the path that works without ARCY. When a flow finishes inside the chat, its Request step can report the same event for you (Flows). If only that step ever sends a flow's completion event, nobody without ARCY can reach it, and Lift shows no difference for that flow until your own code sends it too. There is no server-side way to report an event: call track() from the visitor's browser, after your own request succeeds.

ts
arcy.track("checkout_done")

registerHandler(name, handler)

ts
registerHandler(name: string, handler: (inputs: Record<string, string | number | boolean>) => unknown): () => void

Registers one of your own functions so a flow's Request step can perform it. Use this for an operation your product has no API route for: a Next.js Server Action, a tRPC procedure, a GraphQL mutation, a form post behind CSRF. On Endpoints, that Endpoint's How it is reached is Handler and its Handler name is the name you pass here.

ts
import { createLead } from "@/app/actions/leads"

arcy.registerHandler("create_lead", ({ email, name }) => createLead({ email, name }))

ARCY calls your function with one object: the Endpoint's declared inputs, each already read as its Type and checked against its Rules, and nothing else. No flow key, no visitor id, no attributes. Your page already knows who is signed in, and your own function reads its own session. It reads the Endpoint's Response fields out of what you return, by their dotted paths, and sends nothing you return to ARCY.

Return a value or a promise. A thrown error or a rejected promise is a failure, and so is an answer the Endpoint's How it reports failure matches. A failure sets no run variable and reports no event.

The name is lowercase letters, digits and underscores, starting with a letter, at most 64 characters; a name outside that shape is ignored with one console warning. Registering the same name twice keeps the newest, quietly, so a module reloading in development is not a problem. The return value unregisters that one registration, and only while it is still the current one:

ts
useEffect(() => arcy.registerHandler("create_lead", createLead), [])

Safe to call before init(), and before arcy.js has loaded on the HTML snippet: register at the top of your own script and the call takes effect when arcy.js arrives.

Nothing happens until a flow performs it. A registered function runs only inside a flow a person published, in a step bound to that Endpoint, and a visitor's value reaches it only after passing the Endpoint's Rules.

open() and close()

ts
open(): void
close(): void

Open or close the chat panel from your own code. Both are synchronous. open() opens the chat even for a visitor outside the chat bar audience; closing it takes the bar away again.

VERSION

ts
arcy.VERSION // "0.x.y"

The installed arcy.js version, for a support ticket. arcy.js also sends it when a session starts, which is how the ARCY MCP confirms an update loaded. Every release is on the Changelog. Before 1.0 a release can change how your code must call arcy.js; each such change is listed under "What your code must change", and Update arcy.js on ARCY MCP makes it for you. From 1.0 on, a breaking change means a new major version and advance notice.

TypeScript types

arcy.js ships its own types; there is no @types/arcy.js. Import any of these by name:

ts
import arcy, { type IdentifyOptions, type FlowEventDetail } from "arcy.js"
TypeWhat it is
ArcyThe arcy object itself
ArcyOptionsThe init() options
ListenInstancesThe listen option's object
IdentifyOptionsThe third argument to identify()
IdentifyAnonymousOptionsThe second argument to identifyAnonymous(): { id }
AttributesInputWhat identify(), identifyAnonymous() and updateUser() accept, undefined values included
Attributes, AttributeValue, AttributeObjectAn attribute object, one value, and an Object value
ArcyEvent, ArcyEventHandlerready, open, close and their handler
ArcyFlowEvent, FlowEventHandler, FlowEventDetailThe three flow events, their handler and { key, source }
FlowStartSourceThe eight source values
ConsentState, ConsentHandlerWhat consentState() returns and what onConsent() calls
UnsubscribeWhat on() and onConsent() return

Loading and page changes

The HTML snippet queues until arcy.js loads. Every call made before it loads is held and run in order, so identify() can sit right under the snippet. init(), identify(), identifyAnonymous() and updateUser() return a promise that resolves once arcy.js has loaded and run the call. on() and onConsent() return an unsubscribe that works while the call is still queued. consentState() answers null and VERSION is undefined until then, so read both inside on("ready").

A snippet keeps the methods it was pasted with. A call to a method newer than your snippet, made before arcy.js loads, throws. Call startFlow() from a click or inside on("ready"), which is safe on every snippet, or paste the snippet again from Configure > Installation.

Page changes need no call. ARCY sees every client-side route change on its own, back and forward included, and about half a second after the path changes it checks again which triggers apply there. There is no page view call to make.

An unfinished flow survives a full page load in the same tab, for the same person, within 30 minutes of its last step. Past that, in another tab or after reset(), it does not resume.

arcy.js runs in the browser only. In a server-rendering framework, call it from a client component or a mount effect, never during the server render.

Use one install path per page. Never load the snippet and the npm package on the same page: the second copy is ignored with a warning.

Console warnings

Every line starts with [arcy]. Words in angle brackets, other than <body>, are filled in from your call.

WarningCauseFix
init() needs an environment token. Ignoring the call.init() got no token, or not a string.Pass the Token from Configure > Installation.
init() has already been called. Ignoring the second call.A second init() on one page, often the snippet pasted twice.Keep one install.
init() options must be an object. Continuing without them.The second argument is not an object.Pass an object or nothing.
init() ignored unknown option "<key>". Only locale, defaultOpen, contentLocale, apiBase, requestHeaders, and listen can be set in code; everything else is configured in the dashboard. Consent is given through arcy.consent().An option init() does not take.Remove it. Set it in the dashboard, or call consent() for consent.
init() was given a <tool> instance under listen, but <tool> is not switched on for this environment. Switch it on under Configure > Integrations, or remove it from listen.A dashboard setting.Switch the tool on under Integrations.
<tool> is on this page but its SDK has no surface arcy.js can listen to, so no event from it will be seen. Check the SDK version.Your analytics SDK is a version ARCY cannot hook.Update that SDK.
<method>() was called before init(). Ignoring.identify, identifyAnonymous, updateUser, consent, consentState, onConsent, open or startFlow ran before init().Call init() first.
identify() needs a user id. Ignoring the call.An empty or non-string userId.Pass the signed-in user's id as a string.
<method>() attributes must be an object. Ignoring the call.The attributes argument is not a plain object.Pass an object of keys.
<method>() dropped "<key>": an attribute must be a string, number, boolean, null, an array of strings, an object, or an array of objects.One value of another shape, such as a Date.Send an ISO 8601 string for a date. See Built-in attributes.
identify() was given a userHash that is not a string. Ignoring it.options.userHash is not a string.Pass the hex signature your server computed.
This environment enforces identity verification and no valid userHash was supplied, so this user was not identified and none of their attributes were stored. Pass a signed userHash: arcy.identify(userId, attributes, { userHash }). See Configure > Identity verification in the ARCY dashboard.A dashboard setting: identity verification is on.Sign the user id on your server. See Identity verification.
identifyAnonymous() options must be an object. Ignoring the options.The second argument is not an object.Pass { id } or nothing.
identifyAnonymous() id must be a non-empty string of at most 256 characters. Ignoring the id.options.id is not a string, or is empty or over 256 characters once trimmed.Pass the guest id your Import used in anonymous_id.
updateUser() was called before identify() or identifyAnonymous(). Ignoring the call.No identity to update yet.Call identify() or identifyAnonymous() first.
onConsent() needs a function. Ignoring the call.The handler is not a function.Pass a function.
on("<event>") needs a function. Ignoring.The handler is not a function.Pass a function.
on("<event>") is not an ARCY event. The events are ready, open, close, flow_started, flow_completed and flow_abandoned. Ignoring.A misspelled event name.Use one of the six names.
A "<event>" handler threw. <error>Your handler threw. The other handlers still ran.Fix the handler.
track() dropped "<name>". A name is lowercase letters, digits and underscores, at most 64 characters, starting with a letter.A name outside that shape. Warned once per name.Rename the event.
registerHandler ignored "<name>": a handler name is lowercase letters, digits and underscores, starting with a letter, at most 64 characters.A name outside that shape.Rename it, here and in the Endpoint's Handler name.
registerHandler ignored "<name>": the second argument must be a function.The handler is not a function.Pass a function.
startFlow() needs a Flow key. Ignoring the call.An empty or non-string key.Pass the flow's Key.
startFlow() found no published flow with key "<key>" in this environment.A misspelled or renamed key, a flow not published in this environment, or a visitor outside its audience.Copy the line from flow settings on Flows, and check where the flow is published.
startFlow("<key>") could not load the published flows, so nothing started. Try again, and if it keeps happening send us arcy.VERSION.ARCY could not be reached.Retry. If it repeats, send us arcy.VERSION.
The chat is hidden because this organization has reached its chat cap. Raise or clear the cap under Workspace settings > Limits, or wait for the window to roll over.A dashboard setting: a chat cap was reached.Raise or clear it on Limits.
This page's Content-Security-Policy is blocking part of the ARCY widget.Your page's policy blocks ARCY.See Troubleshooting.
Failed to load. ARCY is disabled. or Failed to load <url>. ARCY is disabled.The script could not load.See Troubleshooting.
Failed to load <url>. The chat panel will not open. If this page sets a Content-Security-Policy, it must allow scripts from <url>.The chat panel's script could not load.See Troubleshooting.
The page has no <body> to mount into yet. The widget will not render.init() ran before the page had a body.See Troubleshooting.
Another copy of ARCY is already on this page. Ignoring this one.The snippet and the npm package, or two snippets, on one page.Keep one install path.
Any other line, such as The chat panel could not load. or The ARCY session could not be started.A failure inside ARCY. Your page keeps working.Send us the line and arcy.VERSION.

See also

On this page