Message API
Make your pet speak, react to named events, and reply through your own endpoint.
Widget Message API
Embed Your Pet widgets should expose the same small interaction contract whether they are generated from a single image, a spritesheet, or a future hosted builder. The goal is to let any website code tell the pet what happened, let the pet show a reply, and leave room for an LLM responder without making EmbedYourPet host customer assets or visitor data by default.
Principles
- The pet is client-side first. A generated widget can reply with local rules without calling EmbedYourPet.
- Site code sends normalized triggers. The widget decides how to display or route a reply.
- LLMs are optional responders. The website owner chooses the endpoint, prompt, logging policy, and consent UX.
- The same contract works through direct JavaScript calls and browser events.
Public Surface
Generated widgets attach themselves to the configured global name, usually window.PetWidget.
<script>
window.PetWidgetOptions = {
scale: 0.7,
corner: "bottom-right",
responder: async function (action, pet) {
return {
title: action.label || "Noted",
body: "I saw " + action.name + ".",
state: "waving"
};
}
};
</script>
<script src="/pixel-widget.js"></script>
After boot:
var pet = window.PetWidget.instance;
pet.say("Welcome in.");
pet.say({ title: "Great click", body: "Nice. I saw that.", state: "waving" });
pet.trigger("cta.clicked", { label: "Start free demo" });
pet.trigger("form.field.focused", {
label: "Email address",
preset: "fieldFocus",
task: { state: "review", duration: 2400 }
});
pet.setResponder(async function (action) {
return fetch("/api/pet-reply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: action.name,
label: action.label,
context: action.context || {}
})
}).then(function (response) {
return response.json();
});
});
pet.clearMessage();
The global also proxies the common methods:
window.PetWidget.say("Tiny announcement.");
window.PetWidget.trigger("checkout.started", { label: "Checkout" });
window.PetWidget.runTask({ state: "waving", duration: 1800 });
window.PetWidget.setResponder(function (action) {
return "Handled " + action.name;
});
Trigger Contract
Use trigger(name, detail) when your site wants the pet to react to something.
window.PetWidget.trigger("docs.copy", {
label: "Install snippet",
preset: "greatClick",
task: {
state: "waving",
duration: 2600
},
message: "Copied. Paste that before the closing body tag.",
context: {
page: location.pathname,
plan: "free"
}
});
Normalized action fields:
type PetAction = {
name: string; // Stable event name, e.g. "cta.clicked"
type: string; // Defaults to name
label?: string; // Human label safe to show or send to a responder
message?: string; // Optional local reply override
preset?: string; // Optional preset reply key
task?: PetTask; // Optional animation/action task
state?: string; // Shorthand for task.state
duration?: number; // Shorthand for task.duration and reply duration
reply?: PetReply; // Optional explicit reply object
context?: object; // Optional structured data for responders
timestamp: number; // Added by the widget when omitted
};
type PetTask = string | {
state?: "idle" | "waiting" | "running" | "runningRight" | "runningLeft" | "waving" | "jumping" | "failed" | "review";
animation?: string; // Alias for state
duration?: number; // Milliseconds before the pet returns to rest
preset?: string; // Optional preset reply to show while running the task
message?: string | PetReply;
};
task is what the pet should do. message is what the pet should say. preset is a reusable local reply/task bundle. A conversion event can use all three.
Recommended event naming:
page.viewedcta.clickedform.focusedform.changedform.submittedcheckout.starteddocs.copiederror.shown
Prefer stable product verbs over CSS selectors. A responder should not need to know that a button is .hero .button:nth-child(2).
Browser Event Fallback
Any code on the page can trigger the pet without holding a reference:
window.dispatchEvent(new CustomEvent("embedyourpet:trigger", {
detail: {
name: "pricing.toggled",
label: "Annual pricing",
preset: "conversion",
task: { state: "waving", duration: 3600 },
message: "Good choice. Annual usually makes the math friendlier."
}
}));
The widget emits lifecycle events on both the pet element and window:
embedyourpet:triggerembedyourpet:messageembedyourpet:messagehide
window.addEventListener("embedyourpet:message", function (event) {
console.log(event.detail.body);
});
Reply Contract
Responders may return a string, null, or a reply object. A promise is supported.
type PetReply = string | null | {
preset?: string;
title?: string;
body?: string;
text?: string;
message?: string;
state?: "idle" | "waiting" | "running" | "runningRight" | "runningLeft" | "waving" | "jumping" | "failed" | "review";
duration?: number;
thinking?: boolean;
task?: PetTask;
};
Rules:
nullmeans "hide or do not show a reply."stateis ignored if the current spritesheet does not define it.body,text, andmessageare aliases. Preferbodyin new code.presetis resolved locally before display and can be overridden by any fields on the reply.tasklets a responder return animation instructions and a message together.- Replies are clamped to the widget's configured max message length before display.
Preset Replies
Generated widgets include a small default preset collection:
window.PetWidget.trigger("lead.email.focused", {
label: "Email",
preset: "fieldFocus"
});
window.PetWidget.trigger("signup.completed", {
label: "Signup",
preset: "conversion",
task: { state: "waving", duration: 4200 }
});
Built-in presets:
greatClickfieldFocusconversionthinkingerror
Site owners can add or override presets before the widget script loads:
<script>
window.PetWidgetOptions = {
presets: {
bookedDemo: {
title: "Demo booked",
body: "Excellent. I will keep an eye on the next step.",
state: "waving",
duration: 5000
}
}
};
</script>
Then trigger it:
window.PetWidget.trigger("demo.booked", {
preset: "bookedDemo"
});
LLM Responder Pattern
The generated widget should not call an LLM directly with an API key in browser code. Use a site-owned endpoint:
window.PetWidget.setResponder(async function (action) {
var response = await fetch("/api/pet-reply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: {
name: action.name,
label: action.label,
context: action.context || {}
}
})
});
if (!response.ok) {
return { preset: "error" };
}
return response.json();
});
Backend endpoints should:
- Send only the fields needed to answer.
- Avoid sending raw form values unless the site has a clear privacy reason and user consent.
- Return a
PetReply. - Prefer returning
presetortaskwhen a known local response is enough. - Set short timeouts and a deterministic fallback reply.
- Rate-limit by session or IP.
Data Attributes
For simple sites, add local message hints to elements and call trigger() from existing handlers:
<button
data-pet-event="cta.clicked"
data-pet-label="Create widget"
data-pet-preset="greatClick"
data-pet-state="waving"
data-pet-duration="2600"
data-pet-message="Good. Let's hatch this little legend.">
Create widget
</button>
<script>
document.addEventListener("click", function (event) {
var target = event.target.closest("[data-pet-event]");
if (!target || !window.PetWidget) return;
window.PetWidget.trigger(target.dataset.petEvent, {
label: target.dataset.petLabel,
preset: target.dataset.petPreset,
message: target.dataset.petMessage,
task: {
state: target.dataset.petState,
duration: Number(target.dataset.petDuration || 0)
}
});
});
</script>
The core widget does not auto-watch the whole page in the Laravel MVP. That keeps behavior predictable for customer exports. A future generator option can add automatic reactions as a layer over this same trigger contract.