Developer Resources

 

Event listeners

If you are using the Standard embed or the Popup, you can add a JavaScript event listener to get notified on specific events sent from the form to your website. The snippets below should be added to your website on the page where the embed or popup will be shown.
 

Form loaded

Since the standard embed and the popup are lazy loaded, you will receive this event whenever the form is loaded i.e. shown to the respondent.
interface LoadedPayload { formId: string; } window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.FormLoaded')) { const payload = JSON.parse(e.data).payload as LoadedPayload; // ... } });
 

Form page view

This is specifically handy if you have a form with multiple pages so you get an event each time the respondent navigates to a page.
interface PageViewPayload { formId: string; page: number; } window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.FormPageView')) { const payload = JSON.parse(e.data).payload as PageViewPayload; // ... } });
 

Form submission

This event is triggered when the form is submitted and it contains not only some metadata, but also the form data (respondent’s 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; }; }>; } window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.FormSubmitted')) { const payload = JSON.parse(e.data).payload as SubmissionPayload; // ... } });
 

Popup closed

This is only for the popup and it is triggered 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; // ... } });
 
 

Embeds

You can programmatically load embeds using a method from the window.Tally object.
// Include the Tally widget script in the <head> section of your page <script src="https://tally.so/widgets/embed.js"></script>
 
Then you would add your embed HTML somewhere on your website:
<iframe data-tally-src="https://tally.so/embed/mRoDv3?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1" loading="lazy" width="100%" height="200" frameborder="0" marginheight="0" marginwidth="0" title="Newsletter"></iframe>
 

Tally.loadEmbeds()

This won’t load the embed yet. To load it call this method:
Tally.loadEmbeds();
Keep in mind, all embeds are instantly loaded if they are in the viewport or 500px away from the viewport’s edges. Otherwise, an observer is set up, which will wait for the embed to satisfy the requirements mentioned above before it get loaded.

Save website page and query parameters

Your website's page and all query parameters will be automatically forwarded to the Tally embed and could be saved using hidden fields. For example, if your page's URL looks like the one below and you have hidden fields for originPageref and email, you will see the following in your form submissions.
https://company.com/register?ref=downloads&[email protected]
originPage
/register
ref
downloads
email
 

Popups

You can open and close popups using JavaScript via the window.Tally object. It comes in handy when you want to define your own business logic on when to open a certain popup.
// Include the Tally widget script in the <head> section of your page <script src="https://tally.so/widgets/embed.js"></script>
 
The Tally object exposes 2 methods, which are defined below. Both of them require the Form ID, which can be found in the URLs of your form:
https://tally.so/r/[FORM_ID] https://tally.so/embed/[FORM_ID] https://tally.so/forms/[FORM_ID]/edit

Tally.openPopup()

// Example Tally.openPopup('mRoDv3', { layout: 'modal' }); // Reference openPopup: ( formId: string, options?: { key?: string; // This is used as a unique identifier used for the "Show only once" and "Don't show after submit" functionality layout?: 'default' | 'modal'; width?: number; alignLeft?: boolean; hideTitle?: boolean; overlay?: boolean; emoji?: { text: string; animation: 'none' | 'wave' | 'tada' | 'heart-beat' | 'spin' | 'flash' | 'bounce' | 'rubber-band' | 'head-shake'; }; autoClose?: number; // in milliseconds showOnce?: boolean; doNotShowAfterSubmit?: boolean; customFormUrl?: string; // when you want to load the form via it's custom domain URL hiddenFields?: { [key: string]: any, }; onOpen?: () => void; onClose?: () => void; onPageView?: (page: number) => void; onSubmit?: (payload: SubmissionPayload) => void; } ) => void;
 

Tally.closePopup()

// Example Tally.closePopup('mRoDv3'); // Reference closePopup: (formId: string) => void;
 

Save website page and query parameters

Your website's page and all query parameters will be automatically forwarded to the Tally popup and could be saved using hidden fields. For example, if your page's URL looks like the one below and you have hidden fields for originPageref and email, you will see the following in your form submissions.
https://company.com/register?ref=downloads&[email protected]
originPage
/register
ref
downloads
email
 

Custom domain code injection

If you refer to the Event listeners above, you can listen to the same events via the code injection on your custom domain. The only difference is that these events are not sent with postMessage but as custom events.
 
interface PageViewPayload { formId: string; page: number; } window.addEventListener('Tally.FormPageView', (payload: PageViewPayload) => { // ... });
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; }; }>; } window.addEventListener('Tally.FormSubmitted', (payload: SubmissionPayload) => { // ... });
 

DOMContentLoaded event handler

If you want to access Tally input elements wrap your code in a DOMContentLoaded event handler.
document.addEventListener('DOMContentLoaded', function () { // Your code goes here // ... });

Get an input element

Let’s assume we want to get the email input element.
document.addEventListener('DOMContentLoaded', function () { // You can find the input's ID by inspecting your form const emailInput = document.getElementById('c1cbc8e4-b2f3-4e63-a683-ec9eadbcb022'); });

Dynamically set a value to an existing input element

In this example, we will get the provided email and use it to fetch the user’s unique identifier.
document.addEventListener('DOMContentLoaded', async function () { // We get the input element const emailInput = document.getElementById('c1cbc8e4-b2f3-4e63-a683-ec9eadbcb022'); // Get the email that was typed in const email = emailInput.value; // Do API lookup here using the email to get the user's unique identifier const response = await fetch(`https://api.example.com/getUserId?email=${email}`); const { userId } = await response.json(); // Set the userID in the input field const userIdInput = document.getElementById('184b69ee-0c9f-449a-b361-b5250ccd2cb3'); // This is necessary to bubble the event up to the input and update the React app state const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value', ).set; nativeInputValueSetter.call(userIdInput, userId); userIdInput.dispatchEvent(new Event('input', { bubbles: true })); });

Get previously entered answers from the browser’s local storage

// You can find the form ID in the URL of the form pages // Example: https://tally.so/forms/mRoDv3/share const formId = 'mRoDv3'; const data = localStorage.getItem(`FORM_DATA_${formId}`); if (data) { data = JSON.parse(data); }
 

React app embed example

import { useEffect } from 'react'; const MyComponent = () => { // The code below will load the embed useEffect(() => { const widgetScriptSrc = 'https://tally.so/widgets/embed.js'; const load = () => { // Load Tally embeds if (typeof Tally !== 'undefined') { Tally.loadEmbeds(); return; } // Fallback if window.Tally is not available document .querySelectorAll('iframe[data-tally-src]:not([src])') .forEach((iframeEl) => { iframeEl.src = iframeEl.dataset.tallySrc; }); }; // If Tally is already loaded, load the embeds if (typeof Tally !== 'undefined') { load(); return; } // If the Tally widget script is not loaded yet, load it if (document.querySelector(`script[src="${widgetScriptSrc}"]`) === null) { const script = document.createElement('script'); script.src = widgetScriptSrc; script.onload = load; script.onerror = load; document.body.appendChild(script); return; } }, []); return ( <div> <iframe data-tally-src="https://tally.so/embed/mRoDv3?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1" loading="lazy" width="100%" height="216" frameBorder={0} marginHeight={0} marginWidth={0} title="Newsletter subscribers" ></iframe> </div> ); }; export default MyComponent;
 

Next.js app embed example

// pages/index.tsx import Script from 'next/script' export default function Homepage() { return ( <div> <iframe data-tally-src="https://tally.so/embed/mRoDv3?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1" loading="lazy" width="100%" height="216" frameBorder={0} marginHeight={0} marginWidth={0} title="Newsletter subscribers" ></iframe> <Script src="https://tally.so/widgets/embed.js" onLoad={() => Tally.loadEmbeds()} /> </div> ); }