Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.tally.so/llms.txt

Use this file to discover all available pages before exploring further.

Every Tally widget emits the same set of events. The transport changes depending on the integration method:
  • Embeds and popups send events as postMessage payloads. Listen for them on the message event on window.
  • Code injection (running inside the form via your custom domain) receives them as CustomEvents dispatched directly on window.
Add your event listeners to the page hosting the embed or popup — not to the form itself. For code injection, add them to the form via your custom domain settings.

Tally.FormLoaded

Fires when the form is rendered. Because embeds and popups are lazy-loaded, you receive this event each time the form is actually shown.
interface LoadedPayload {
  formId: string;
}

window.addEventListener('message', (e) => {
  if (e?.data?.includes('Tally.FormLoaded')) {
    const payload = JSON.parse(e.data).payload as LoadedPayload;
    // ...
  }
});

Tally.FormPageView

Fires every time the respondent navigates to a page of the form — handy for multi-page forms.
interface PageViewPayload {
  formId: string;
  page: number;
}

// For embeds and popups
window.addEventListener('message', (e) => {
  if (e?.data?.includes('Tally.FormPageView')) {
    const payload = JSON.parse(e.data).payload as PageViewPayload;
    // ...
  }
});

// For code injection via a custom domain
window.addEventListener('Tally.FormPageView', (e) => {
  const payload = e.detail as PageViewPayload;
  // ...
});

Tally.FormSubmitted

Fires when the form is submitted. The payload contains the submission metadata and the full set of answers.
interface SubmissionPayload {
  id: string; // submission ID
  respondentId: string;
  formId: string;
  formName: string;
  createdAt: Date; // submission date
  fields: Array<{
    id: string;
    title: string;
    type:
      | 'INPUT_TEXT'
      | 'INPUT_NUMBER'
      | 'INPUT_EMAIL'
      | 'INPUT_PHONE_NUMBER'
      | 'INPUT_LINK'
      | 'INPUT_DATE'
      | 'INPUT_TIME'
      | 'TEXTAREA'
      | 'MULTIPLE_CHOICE'
      | 'DROPDOWN'
      | 'CHECKBOXES'
      | 'LINEAR_SCALE'
      | 'FILE_UPLOAD'
      | 'HIDDEN_FIELDS'
      | 'CALCULATED_FIELDS'
      | 'RATING'
      | 'MULTI_SELECT'
      | 'MATRIX'
      | 'RANKING'
      | 'SIGNATURE'
      | 'PAYMENT';
    answer: { value: any; raw: any };
  }>;
}

// For embeds and popups
window.addEventListener('message', (e) => {
  if (e?.data?.includes('Tally.FormSubmitted')) {
    const payload = JSON.parse(e.data).payload as SubmissionPayload;
    // ...
  }
});

// For code injection via a custom domain
window.addEventListener('Tally.FormSubmitted', (e) => {
  const payload = e.detail as SubmissionPayload;
  // ...
});

Tally.PopupClosed

Fires only for popups, when the popup is closed.
interface PopupClosedPayload {
  formId: string;
}

window.addEventListener('message', (e) => {
  if (e?.data?.includes('Tally.PopupClosed')) {
    const payload = JSON.parse(e.data).payload as PopupClosedPayload;
    // ...
  }
});
Popups also accept lifecycle callbacks (onOpen, onClose, onPageView, onSubmit) directly on the Tally.openPopup() options argument. See Popups for the full reference.