Skip to content

Listen to widget events

Know when users submit feedback, vote, or comment without polling. The widget SDK fires events you can subscribe to with on() and off(), so your app can trigger analytics, show confirmations, or update its own UI in response.

Subscribe to events

Use Quackback("on", eventName, handler) to listen for events. It returns an unsubscribe function:

const unsubscribe = Quackback("on", "post:created", (payload) => {
  console.log("New post:", payload.title);
  analytics.track("feedback_submitted", { postId: payload.id });
});
 
// Later, stop listening:
unsubscribe();

You can also remove listeners with off():

function handleVote(payload) {
  console.log(payload.voted ? "Upvoted" : "Removed vote", payload.postId);
}
 
// Start listening
Quackback("on", "vote", handleVote);
 
// Remove a specific handler
Quackback("off", "vote", handleVote);
 
// Remove all handlers for an event
Quackback("off", "vote");

Available events

ready

Fires when the widget iframe has loaded and is ready to receive commands.

Quackback("on", "ready", () => {
  console.log("Widget is ready");
});

Payload: {}

open

Fires when the widget panel opens (via the trigger button or Quackback("open")).

Quackback("on", "open", () => {
  document.body.classList.add("widget-open");
});

Payload: {}

close

Fires when the widget panel closes.

Quackback("on", "close", () => {
  document.body.classList.remove("widget-open");
});

Payload: {}

post:created

Fires after a user submits a new post through the widget.

Quackback("on", "post:created", (payload) => {
  showToast(`Thanks for your feedback: "${payload.title}"`);
});

Payload:

FieldTypeDescription
idstringPost ID
titlestringPost title
board{ id, name, slug }Board the post was created in
statusIdstring | nullInitial status ID

vote

Fires when a user votes or removes their vote on a post.

Quackback("on", "vote", (payload) => {
  analytics.track("vote_toggled", {
    postId: payload.postId,
    voted: payload.voted,
  });
});

Payload:

FieldTypeDescription
postIdstringPost ID
votedbooleantrue if voted, false if unvoted
voteCountnumberUpdated vote count

comment:created

Fires when a user posts a comment or reply.

Quackback("on", "comment:created", (payload) => {
  console.log("Comment added to post", payload.postId);
});

Payload:

FieldTypeDescription
postIdstringPost the comment belongs to
commentIdstringNew comment ID
parentIdstring | nullParent comment ID for replies, null for root comments

identify

Fires after an identify() call completes (success or failure).

Quackback("on", "identify", (payload) => {
  if (payload.success) {
    console.log("Identified:", payload.user?.name ?? "anonymous");
  } else {
    console.error("Identify failed:", payload.error);
  }
});

Payload:

FieldTypeDescription
successbooleanWhether identification succeeded
user{ id, name, email } | nullUser details (null for anonymous)
anonymousbooleantrue if this is an anonymous session
errorstring | undefinedError code on failure

SPA usage

In React or other frameworks, subscribe in an effect and clean up on unmount:

import { useEffect } from "react";
 
export function WidgetEvents() {
  useEffect(() => {
    const unsubs = [
      Quackback("on", "post:created", (p) => {
        analytics.track("feedback_submitted", { postId: p.id });
      }),
      Quackback("on", "vote", (p) => {
        analytics.track("vote", { postId: p.postId, voted: p.voted });
      }),
    ];
 
    return () => unsubs.forEach((fn) => fn());
  }, []);
 
  return null;
}

Event handlers are called synchronously. If you need to do async work (API calls, etc.), wrap the body in an async IIFE or fire-and-forget promise. Errors inside handlers are caught and swallowed to prevent breaking the widget.

Next steps