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.viewed
  • cta.clicked
  • form.focused
  • form.changed
  • form.submitted
  • checkout.started
  • docs.copied
  • error.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:trigger
  • embedyourpet:message
  • embedyourpet: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:

  • null means "hide or do not show a reply."
  • state is ignored if the current spritesheet does not define it.
  • body, text, and message are aliases. Prefer body in new code.
  • preset is resolved locally before display and can be overridden by any fields on the reply.
  • task lets 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:

  • greatClick
  • fieldFocus
  • conversion
  • thinking
  • error

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 preset or task when 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.