FieldStone SDK
v0.2.7Embed an assistant on any website. Drop in the chat widget for booking and service questions, or wire your own forms straight to your inbox — both powered by your FieldStone account.
Chat Widget
A floating assistant that chats, qualifies visitors, and books appointments. One script tag, or a React component.
Set up the widget →Free Agent Forms
Headless — keep your own form UI and POST submissions straight to the email and phone bound to your key. No widget, no CRM.
Send a submission →Numbers API
Rent a real phone number and drive it from your own server. Send and receive SMS and MMS, poll one loop for replies and delivery updates, or let us send on a schedule. No FieldStone account needed.
Read the Numbers API docs →Get your credentials
Every snippet below authenticates with two values from a FieldStone account. Grab them once and drop them into FieldStone.init().
- Business ID — identifies the business the assistant answers for. Required for the chat widget; replaces the
your-business-idplaceholder. - API key — a secret that starts with
fsk_. Required for free-agent forms and recommended for the widget; replacesfsk_your_api_key.
Where to find them
- In FieldStone, open Settings → SDK.
- Copy your Business ID from the top of the page.
- Click Generate Keyto create an API key. It's shown once— copy it immediately; for security it can't be retrieved later. You can keep up to 3 active keys and revoke any of them anytime.
Integrating on behalf of a business? Ask the account owner to send you their Business ID and a freshly generated API key from that screen. Free-agent keys are issued separately by a FieldStone super admin, bundled with the notify email and phone — see Free Agent Forms. Keep keys private and never commit them to a public repository.
Chat Widget
Quick Start
Add these two lines before your closing </body> tag. Works with any website — no build step required. Swap in your Business ID and API key.
<script src="https://pub-cc5ed544ca014859991114c0d7808ed7.r2.dev/v1/sdk.js"></script>
<script>
FieldStone.init({
businessId: "your-business-id",
apiKey: "fsk_your_api_key",
});
</script>Install via Package Manager
For React, Vue, Svelte, or any project with a build step.
npm install @fieldstoneapp/sdk
React
Import the component and wrap your app.
import { FieldStoneAI } from "@fieldstoneapp/sdk";
function App() {
return (
<FieldStoneAI
businessId="your-business-id"
apiKey="fsk_your_api_key"
>
<YourApp />
</FieldStoneAI>
);
}Configuration
Options passed to FieldStone.init() or the <FieldStoneAI> component.
| Option | Type | Description |
|---|---|---|
| businessId* | string | Your business identifier. Required for the chat widget; omit it in free-agent mode. |
| apiKey | string | Your SDK API key from Settings → SDK. Defaults to businessId for public-key flows. Required in free-agent mode. |
| mode | string | "widget" (default) mounts the chat widget. "free_agent" and "headless" mount nothing — no widget, no bootstrap, only submitForm(). See Free Agent Forms below. |
| proactive | object | Overrides the proactive-teaser rules set in Settings → SDK. Off by default. See Proactive engagement below. |
| analytics | object | { dataLayer: boolean } — push fieldstone_open / fieldstone_booked onto window.dataLayer for GTM/GA4. Skipped for visitors signalling GPC or DNT. See Events below. |
| apiUrl | string | Override the FieldStone API base URL. Only needed for self-hosted or staging environments. |
| position | string | "bottom-right", "bottom-left", "top-right", or "top-left". |
| theme | object | Override colors: primaryColor, fontFamily, borderRadius, dark. Set hideBranding: true to remove the “Powered by FieldStone” footer. |
| voice | object | { enabled: boolean } — toggle the push-to-talk mic button in the chat input. |
| context | string | Extra business info (pricing, policies, hours) sent with every message. Silently capped at 2000 characters. See Business Context below. |
Programmatic Control
The widget mounts itself and runs on its own, but you can drive it from your own UI via the singleton on FieldStone.getInstance(). A second FieldStone.init() call warns and returns the existing instance.
// Open / close the chat panel from your own UI FieldStone.getInstance()?.open(); FieldStone.getInstance()?.close(); // Wait for the bootstrap call to finish const business = await FieldStone.getInstance()?.ready(); // Tear it down completely (e.g. on logout) FieldStone.getInstance()?.destroy();
Events
Subscribe to what the widget does so bookings show up in your own analytics. on() returns an unsubscribe function, and every listener is dropped on destroy().
const sdk = FieldStone.getInstance();
// Forward the conversion to your own analytics.
sdk.on("booked", ({ title, secondsFromFirstMessage }) => {
gtag("event", "generate_lead", {
item_name: title,
engagement_time_msec: (secondsFromFirstMessage ?? 0) * 1000,
});
});
// on() returns an unsubscribe function.
const off = sdk.on("open", () => console.log("panel opened"));
off();
// Or skip the callbacks and let the SDK push to GTM for you.
FieldStone.init({
businessId: "your-business-id",
apiKey: "fsk_your_api_key",
analytics: { dataLayer: true },
});| Event | Payload |
|---|---|
ready | The business context returned by bootstrap (name, branding, hours). |
open | { secondsSinceLoad } — how long the visitor browsed before opening. |
close | No payload. |
message | { role: "user" | "assistant", length } — character count only, never the text. |
booked | { title, message, secondsFromFirstMessage } — fires only on a successful booking. |
error | { phase: "init" | "chat" | "transcribe" | "booking", message }. |
- No message content ever leaves in an event.
messagecarries a character count so you can measure engagement without piping a visitor's words into a third party. - Listeners fire for every visitor. They're your own first-party callbacks — you already run JavaScript on that page.
- Prefer a tag manager? Set
analytics: { dataLayer: true }and the SDK pushesfieldstone_openandfieldstone_bookedontowindow.dataLayer. That path routes to third parties, so it is skipped for visitors signalling Global Privacy Control or Do Not Track.
Proactive engagement
The widget can show a small, dismissible note beside the bubble when a visitor lingers, scrolls past a point, or looks like they're leaving. It's off by default, and normally configured in Settings → SDK so the rules change without touching your site — the snippet below is the code-level override.
// Optional — most operators set this in Settings → SDK instead,
// so the rules can change without editing the website.
FieldStone.init({
businessId: "your-business-id",
apiKey: "fsk_your_api_key",
proactive: {
enabled: true,
dwellSeconds: 20, // 0 turns this trigger off
exitIntent: true, // desktop only
scrollPercent: 0, // 1–100, 0 turns it off
message: "Questions? I'm here.",
rules: [
{ pattern: "/pricing", message: "Questions about pricing?" },
{ pattern: "/services/*", message: "Want a quote for this?" },
],
cooldownDays: 7, // silence after a visitor dismisses it
},
});rulesare matched against the pathname, first match wins. A trailing*is a prefix wildcard; an empty pattern matches every page. With no match it falls back tomessage, then to your assistant's greeting.- It shows at most once per visit, never after the visitor has opened the chat themselves, and never again for
cooldownDaysonce someone dismisses it. - Never shown on phones or tablets, where the panel is a full-screen sheet and a teaser would take over the page.
- Never shown to visitors signalling Global Privacy Control or Do Not Track. They asked not to be tracked; we read that as “don't interrupt me” too.
- Both halves are measured — Settings → SDK → Insights reports how often it was shown, how many conversations it started, and how many people closed it. Watch the dismiss rate.
Business Context
Pass short, business-specific facts that the assistant should know on every turn — pricing rules, service minimums, seasonal hours, promotions. The string is forwarded to the agent as background data (not instructions), so it's a safe place for facts, not for telling the assistant how to behave.
FieldStone.init({
businessId: "your-business-id",
apiKey: "fsk_your_api_key",
context: `
Emergency plumbing calls answered 24/7.
Minimum call-out fee: $150 (waived if you book a full repair).
Service area: within 40 miles of downtown.
`,
});
// Update context later — the next message the visitor sends
// will include the new value.
FieldStone.getInstance()?.setContext("Updated pricing: $175 minimum.");- Soft cap of 2000 characters. Anything longer is silently truncated server-side — no error is thrown.
- Read at send time, so
setContext()updates apply to the very next message. - Not persisted across page reloads. Set it on init, or refresh it whenever your underlying data changes.
- Use it for facts("minimum fee $150"), not behavior ("always upsell"). Assistant tone and personality belong in Settings → Assistant.
Free Agent Forms
Headless submissions
Keep your own form and design. A free-agent keyruns headless — no chat widget mounts and there's no bootstrap call. You collect your own fields and submit them; the server fans each submission out to the email and phone bound to the key, and keeps a copy for 90 daysso you can read it back in your lead portal (below). Destinations are server-bound to the key and can't be set from the client.
Keys are issued by a FieldStone super admin together with the notify email, phone, and logo. The same key is used in every snippet below.
Plain fetch
No SDK required. POST your fields with the key in the X-API-Key header.
await fetch(
"https://stone-production-d2ea.up.railway.app/api/sdk/free-agent/submit",
{
method: "POST",
headers: {
"X-API-Key": "fsk_your_free_agent_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
fields: [
// "Name" drives the subject, "Email" drives Reply-To.
{ label: "Name", value: "Jane Doe" },
{ label: "Email", value: "jane@example.com" },
{ label: "Project", value: "Kitchen remodel" },
],
images: ["https://example.com/site-photo.jpg"], // optional, https only
}),
},
);Script tag (headless)
Initialize with mode: "free_agent" and call FieldStone.submitForm() from your own submit handler. No chatbot appears.
<script src="https://pub-cc5ed544ca014859991114c0d7808ed7.r2.dev/v1/sdk.js"></script>
<script>
// Headless: no chat widget mounts, no bootstrap call.
FieldStone.init({
apiKey: "fsk_your_free_agent_key",
mode: "free_agent",
});
// Submit from your own form's handler.
FieldStone.submitForm({
fields: [
{ label: "Name", value: "Jane Doe" },
{ label: "Email", value: "jane@example.com" },
],
images: [],
});
</script>React hook
useFreeAgentSubmit lazily initializes a headless instance and returns submit plus loading / error state.
import { useFreeAgentSubmit } from "@fieldstoneapp/sdk";
function ContactForm() {
const { submit, loading, error } = useFreeAgentSubmit({
apiKey: "fsk_your_free_agent_key",
});
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
const result = await submit({
fields: [
{ label: "Name", value: "Jane Doe" },
{ label: "Email", value: "jane@example.com" },
],
images: [],
});
// result → { ok, email, sms }
}
return (
<form onSubmit={onSubmit}>
{/* your own fields */}
<button type="submit" disabled={loading}>Send</button>
{error && <p>{error.message}</p>}
</form>
);
}Response & limits
A successful submit resolves with each channel's dispatch result. A non-2xx response rejects (the hook surfaces it as error).
{ "ok": true, "email": true, "sms": true }- fields — up to 20
{ label, value }pairs. Labels max 100 chars, values max 2000. - images — optional, up to 10
https://URLs, embedded inline in the notification email. - 409— the key has no notification email configured, so it can't deliver. Ask the admin who issued it to set a destination.
- honeypot— optional bot trap. Render an input a human can't see or tab to (off-screen,
aria-hidden,tabindex="-1") and pass whatever it holds. Anything non-empty is accepted and silently discarded — the response is deliberately identical to a success, so a bot can't detect the trap. - 429 — more than 5 submissions per minute from one IP, or more than 20 per minute for a single key. Back off and retry.
- 502 — the submission was accepted but no channel delivered. It will still appear in your portal, but nobody was notified — treat it as a failure and retry.
- Submissions that look automated are still delivered, with a
[Likely spam]subject prefix. Obvious bot traffic is discarded silently. - The submitted fields and image URLs are stored for 90 daysto back your lead portal, then deleted automatically. No contact is created, no image file is copied to our storage, and nothing is added to anyone's CRM. An anonymous
free_agent_submittedusage event (counts + origin) is recorded alongside it.
What the notification looks like
The email is the primary delivery. It arrives from your own business name, and its subject and Reply-To are derived from the fields you send — so what you name your fields changes what lands in the inbox.
From: Acme Co. via FieldStone <form-submissions@fieldstoneapp.com> Reply-To: Jane Doe <jane@example.com> Subject: New lead from Jane Doe Name Jane Doe Email jane@example.com Project Kitchen remodel
- From — your business name, then
form-submissions@fieldstoneapp.com. The name is set on the key by the admin who issued it. If you filter or whitelist on the sender, use that address — these notifications no longer come fromsystem@fieldstoneapp.com. - Subject — named after whoever filled the form. Send a field labelled
Name,Full name,Your name, or aFirst name/Last namepair and you get New lead from Jane Doe. Without one it falls back to New lead from your website. Casing, spacing and punctuation don't matter —your_nameandYour Name *both match. - Reply-To — set to the submitter when you send a field labelled
Email(any label containing “email” works), so hitting Reply answers the lead directly. If no field is labelled that way we fall back to the first value that parses as an address. Nobody reads theform-submissions@mailbox, so a form with no email field leaves the recipient no way to reply — include one. - On a submission flagged as likely automated, the subject is prefixed
[Likely spam]andReply-Tois deliberately omitted — that address is exactly what a spammer controls. - SMS — a short nudge that deep-links the new lead. It carries no field values by design, since it lands on a lock screen.
Your lead portal
Every free-agent key has a private portal at /leads/<token> listing the last 90 days of submissions, with a page per lead and a simple stats view. No account and no password — the link itself is the credential, so treat it like one.
- The email we send you links to the full list; the SMS links straight to the lead that just came in.
- You can archive a lead to clear it from the list. There is no delete — the 90-day expiry is what removes it, and your emails stay your permanent copy.
- Revoking the key turns the portal off with it. If the link gets out, ask the admin who issued the key to reissue it.
Capture submissions into your CRM
Use the same submitForm({ fields, images }) call with your own account API key (from Settings → SDK) instead of a free-agent key. Rather than an email relay, each submission is saved into your CRM: a contact is created or matched, the full field list is logged as a note, and any image URLs are downloaded into StoneBox and linked to the contact.
The identity fields Name, Email, Phone, and Address map onto the contact (matched by email, then phone); every field is kept verbatim in the note. The submit resolves with a photo count instead of the notify flags.
{ "ok": true, "photosSaved": 1 }The response no longer includes contactId or created. Your API key travels in the browser, so returning whether a submitted email already matched a contact would have let anyone probe your customer list.
More
Privacy
The widget never collects identifying data until the visitor explicitly enters it (name, phone, email) during a booking. It also respects browser-level privacy signals at init time:
- Global Privacy Control (
navigator.globalPrivacyControl === true) - Do Not Track (
navigator.doNotTrack === "1")
When either signal is set, all client-side analytics calls become no-ops for the entire session. The chat itself keeps working.
Need help? fieldstoneapp@gmail.com