Tally
Integrate Tally forms via its API and embed SDK to handle submissions, configure
You are an expert at integrating Tally, the free-first form builder, into applications. You retrieve submissions programmatically, react to new entries via webhooks, and embed Tally forms into web apps with proper event handling. ## Key Points - **Relying on field array order** instead of matching by `key` or `label`. Field order can change when the form is edited. - **Skipping webhook signature verification**. Any external actor can POST to your endpoint with fabricated data. - **Not handling duplicate `eventId` values**. Tally may retry webhook delivery, causing duplicate processing. - **Embedding with a plain iframe tag** without loading the Tally embed script. You lose auto-resize, hidden fields, and popup support. - Collecting structured feedback, applications, or registrations without building custom form UI. - Processing form submissions in real time via webhooks to trigger workflows. - Embedding forms directly into a product for seamless user experience. - Building lightweight survey pipelines where Tally handles the UI and your backend handles the data. - Projects that need a free form builder with conditional logic and calculations. ## Quick Example ```bash TALLY_API_KEY=tally_... TALLY_SIGNING_SECRET=whsec_... # webhook signature verification ```
skilldb get form-survey-services-skills/TallyFull skill: 241 linesTally Forms API Skill
You are an expert at integrating Tally, the free-first form builder, into applications. You retrieve submissions programmatically, react to new entries via webhooks, and embed Tally forms into web apps with proper event handling.
Core Philosophy
Simplicity Over Configuration
Tally forms are designed in a Notion-like block editor. Keep API integrations lightweight — use webhooks for real-time processing rather than building complex polling infrastructure. Tally handles conditional logic, calculations, and payments natively; your backend just consumes the results.
Webhook-Centric Architecture
Tally's primary integration path is webhooks. Every form submission fires a POST to your endpoint with structured field data. Design your backend to be idempotent on the eventId and parse field values by their key or label.
Embed-First for Seamless UX
Use Tally's JavaScript embed library or popup mode rather than linking to external Tally URLs. Embedded forms inherit your site's context and allow you to capture submission events for SPA routing or analytics.
Setup
Configure your API access and webhook endpoint:
const TALLY_API_KEY = process.env.TALLY_API_KEY;
const BASE_URL = "https://api.tally.so";
async function tallyFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
...init,
headers: {
Authorization: `Bearer ${TALLY_API_KEY}`,
"Content-Type": "application/json",
...init?.headers,
},
});
if (!res.ok) throw new Error(`Tally ${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
}
Environment variables:
TALLY_API_KEY=tally_...
TALLY_SIGNING_SECRET=whsec_... # webhook signature verification
Key Patterns
Handle Webhook Submissions
Do this — verify the signature and extract fields by key:
import crypto from "node:crypto";
interface TallyWebhookPayload {
eventId: string;
eventType: "FORM_RESPONSE";
createdAt: string;
data: {
responseId: string;
formId: string;
formName: string;
fields: Array<{
key: string;
label: string;
type: "INPUT_TEXT" | "INPUT_EMAIL" | "INPUT_NUMBER" | "MULTIPLE_CHOICE" | "TEXTAREA" | "CHECKBOXES";
value: unknown;
}>;
};
}
function verifyTallySignature(payload: string, signature: string, secret: string): boolean {
const computed = crypto.createHmac("sha256", secret).update(payload).digest("base64");
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature));
}
app.post("/webhooks/tally", (req, res) => {
const signature = req.headers["tally-signature"] as string;
if (!verifyTallySignature(JSON.stringify(req.body), signature, process.env.TALLY_SIGNING_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const event = req.body as TallyWebhookPayload;
const emailField = event.data.fields.find((f) => f.type === "INPUT_EMAIL");
await processResponse(event.eventId, emailField?.value as string);
res.status(200).send("OK");
});
Not this — parsing fields by array index instead of key/type, which breaks when form fields are reordered.
Retrieve Submissions via API
Do this — paginate through responses with cursor-based pagination:
interface TallySubmissionsResponse {
page: number;
totalNumberOfPages: number;
submissions: Array<{
responseId: string;
submittedAt: string;
fields: Array<{ key: string; label: string; value: unknown }>;
}>;
}
async function getAllSubmissions(formId: string): Promise<TallySubmissionsResponse["submissions"]> {
const all: TallySubmissionsResponse["submissions"] = [];
let page = 1;
let totalPages = 1;
while (page <= totalPages) {
const data = await tallyFetch<TallySubmissionsResponse>(
`/forms/${formId}/submissions?page=${page}&limit=50`
);
all.push(...data.submissions);
totalPages = data.totalNumberOfPages;
page++;
}
return all;
}
Not this — fetching only the first page and assuming all data is returned.
Embed Tally Forms in React
Do this — use the Tally embed script with submission event callbacks:
import { useEffect } from "react";
function TallyEmbed({ formId, onSubmit }: { formId: string; onSubmit?: () => void }) {
useEffect(() => {
// Load the Tally embed script
const script = document.createElement("script");
script.src = "https://tally.so/widgets/embed.js";
script.async = true;
document.head.appendChild(script);
// Listen for submission events
function handleMessage(event: MessageEvent) {
if (event.data?.event === "Tally.FormSubmitted" && event.data?.payload?.formId === formId) {
onSubmit?.();
}
}
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
document.head.removeChild(script);
};
}, [formId, onSubmit]);
return (
<iframe
data-tally-src={`https://tally.so/embed/${formId}?alignLeft=1&hideTitle=1&transparentBackground=1`}
width="100%"
height="500"
frameBorder={0}
title="Tally Form"
/>
);
}
Not this — linking users to an external tally.so/r/... URL, losing context and submission callbacks.
Common Patterns
List All Forms
interface TallyForm {
id: string;
name: string;
status: "PUBLISHED" | "DRAFT";
createdAt: string;
numberOfSubmissions: number;
}
const forms = await tallyFetch<{ forms: TallyForm[] }>("/forms");
Open as Popup
// After loading the Tally embed script
declare const Tally: { openPopup: (formId: string, options?: Record<string, unknown>) => void };
Tally.openPopup("mYf0rM", {
width: 700,
emoji: { text: "wave", animation: "wave" },
hiddenFields: { userId: currentUser.id },
onSubmit: (payload: { responseId: string }) => {
console.log("Submitted:", payload.responseId);
},
});
Map Field Values to a Typed Object
function mapTallyFields<T extends Record<string, string>>(
fields: TallyWebhookPayload["data"]["fields"],
keyMap: T
): Record<T[keyof T], unknown> {
const result = {} as Record<string, unknown>;
for (const [tallyKey, outputKey] of Object.entries(keyMap)) {
const field = fields.find((f) => f.key === tallyKey);
result[outputKey] = field?.value ?? null;
}
return result;
}
const mapped = mapTallyFields(event.data.fields, {
question_email: "email",
question_name: "name",
question_rating: "rating",
});
Anti-Patterns
- Relying on field array order instead of matching by
keyorlabel. Field order can change when the form is edited. - Skipping webhook signature verification. Any external actor can POST to your endpoint with fabricated data.
- Not handling duplicate
eventIdvalues. Tally may retry webhook delivery, causing duplicate processing. - Embedding with a plain iframe tag without loading the Tally embed script. You lose auto-resize, hidden fields, and popup support.
When to Use
- Collecting structured feedback, applications, or registrations without building custom form UI.
- Processing form submissions in real time via webhooks to trigger workflows.
- Embedding forms directly into a product for seamless user experience.
- Building lightweight survey pipelines where Tally handles the UI and your backend handles the data.
- Projects that need a free form builder with conditional logic and calculations.
Install this skill directly: skilldb add form-survey-services-skills
Related Skills
Formbricks
Integrate Formbricks open-source surveys for in-app, website, and link-based
Formik
Build React forms with Formik using useFormik hook, Field components, and
Jotform
Integrate JotForm's REST API to create forms, retrieve submissions, process
React Hook Form
Build performant React forms with React Hook Form using register, validation,
Surveymonkey
Integrate SurveyMonkey's REST API to create surveys, collect responses via
Typeform
Integrate Typeform APIs to create forms, retrieve responses, configure webhooks,