Listen to widget events

React to user actions inside the widget with event callbacks.

JM
James Morton
Written By James MortonLast updated 11 days ago

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 launcher 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:

Field

Type

Description

id

string

Post ID

title

string

Post title

board

{ id, name, slug }

Board the post was created in

statusId

string | null

Initial 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:

Field

Type

Description

postId

string

Post ID

voted

boolean

true if voted, false if unvoted

voteCount

number

Updated 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:

Field

Type

Description

postId

string

Post the comment belongs to

commentId

string

New comment ID

parentId

string | null

Parent 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:

Field

Type

Description

success

boolean

Whether identification succeeded

user

{ id, name, email } | null

User details (null for anonymous)

anonymous

boolean

true if this is an anonymous session

error

string | undefined

Error 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;
}

Tip: 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

Was this helpful?

Your feedback shapes what we write next.