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?)
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.
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 }
}| Option | Value | Effect |
|---|---|---|
locale | "en", "tr", "de" or "ru" | The language the agent replies in by default. Any other value is ignored. Does not choose translated content. |
defaultOpen | true or false | Opens 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. |
apiBase | An origin, or an origin with a path prefix, no trailing slash | Where 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. |
requestHeaders | A function returning a plain object of headers, or a promise of one | Called 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. |
listen | An object naming the PostHog, Mixpanel or Amplitude instance your code created | For 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. |
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?)
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:
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.
| Key | Type | Accepted value |
|---|---|---|
user_email | Text | Up to 1,000 characters. |
user_first_name | Text | Up to 1,000 characters. Send the two halves, never one full name. |
user_last_name | Text | Up to 1,000 characters. |
user_signed_up_at | Datetime | ISO 8601 text, such as user.createdAt.toISOString(). Stored in UTC. |
organization_id | Text | The account the user belongs to. Users sharing it roll up into one organization. |
organization_name | Text | The account's name. |
organization_signed_up_at | Datetime | ISO 8601 text. |
plan_name | Text | The plan's name. |
plan_value | Number | A number of 0 or more, never text: 249, not "249". |
plan_cycle | Text | monthly or annual, nothing else. |
plan_currency | Text | One 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?)
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:
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?)
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()
reset(): voidClears 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)
consent(granted: boolean): voidRecords 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()
consentState(): "granted" | "refused" | nullThe 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)
onConsent(handler: (granted: boolean) => void): () => voidFires 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)
on(event: "ready" | "open" | "close", handler: () => void): () => void
on(
event: "flow_started" | "flow_completed" | "flow_abandoned",
handler: (detail: { key: string; source: FlowStartSource }) => void,
): () => voidSubscribes to an event and returns an unsubscribe function. Callable before init(), and
reset() keeps every subscription.
| Event | Fires when | Handler receives |
|---|---|---|
ready | init() has configured the session. | Nothing |
open, close | The chat panel opens or closes. | Nothing |
flow_started | Any flow starts, whoever started it. | { key, source } |
flow_completed | A flow reaches its end. | { key, source } |
flow_abandoned | A 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:
source | Started by |
|---|---|
starter | A Starter Flow pill on the bar or the welcome screen |
drawer | The Flows drawer in the chat |
shortcut | A flow offered in the chat transcript |
target_suggest | A visitor accepting an Agent's Suggest |
target_run | An Agent's Run |
checklist | A Checklist item |
code | Your own startFlow() call |
flow_route | ARCY 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:
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)
startFlow(key: string): voidStarts 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.
<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)
track(name: string): voidReports 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.
arcy.track("checkout_done")registerHandler(name, handler)
registerHandler(name: string, handler: (inputs: Record<string, string | number | boolean>) => unknown): () => voidRegisters 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.
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:
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()
open(): void
close(): voidOpen 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
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:
import arcy, { type IdentifyOptions, type FlowEventDetail } from "arcy.js"| Type | What it is |
|---|---|
Arcy | The arcy object itself |
ArcyOptions | The init() options |
ListenInstances | The listen option's object |
IdentifyOptions | The third argument to identify() |
IdentifyAnonymousOptions | The second argument to identifyAnonymous(): { id } |
AttributesInput | What identify(), identifyAnonymous() and updateUser() accept, undefined values included |
Attributes, AttributeValue, AttributeObject | An attribute object, one value, and an Object value |
ArcyEvent, ArcyEventHandler | ready, open, close and their handler |
ArcyFlowEvent, FlowEventHandler, FlowEventDetail | The three flow events, their handler and { key, source } |
FlowStartSource | The eight source values |
ConsentState, ConsentHandler | What consentState() returns and what onConsent() calls |
Unsubscribe | What 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.
| Warning | Cause | Fix |
|---|---|---|
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. |