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

Examples

Embed in a React app

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;
 

Embed in Next.js app

// 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> ); }

Collect the respondent’s IP address

🔐
Privacy and Security: Fetching the visitor’s IP address raises privacy concerns. Make sure to inform users and obtain consent if necessary.
This example requires the use of a custom domain and code injection. Copy the code below to the Code injection box of your custom domain and replace the input’s ID with your own.
const getIPAddress = async () => { try { const response = await fetch('https://api.ipify.org?format=json'); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); return data.ip; } catch (error) { console.error('Error fetching the IP address:', error); throw error; } }; document.addEventListener('DOMContentLoaded', async () => { // Create a Short text field at the beginning of your form, we will use it to collect the IP address of the visitor // You can find the input's ID by inspecting your form in Preview mode, for example const ipInput = document.getElementById( 'c1cbc8e4-b2f3-4e63-a683-ec9eadbcb022' ); if (!ipInput) { return; } // Hide the input so it's not visible in the form ipInput.style.display = 'none'; // Get the IP address of the visitor const ip = await getIPAddress(); // 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(ipInput, ip); ipInput.dispatchEvent(new Event('input', { bubbles: true })); });