Tally
Integrate Tally forms via its API and embed SDK to handle submissions, configure webhooks, apply conditional logic, and embed forms seamlessly. Covers REST endpoints, webhook payloads, and front-end embedding with TypeScript.
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: 240 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
Typeform
Integrate Typeform APIs to create forms, retrieve responses, configure webhooks, set up logic jumps, and embed forms in web applications. Covers the Create API, Responses API, and front-end embed SDK with TypeScript patterns.
Zod
Define and validate data schemas with Zod for parsing, transforms, refinements, and form integration. Covers schema composition, type inference, custom error messages, and integration with React Hook Form and tRPC.
Formbricks
Integrate Formbricks open-source surveys for in-app, website, and link-based feedback collection. Covers the JavaScript SDK for targeted in-app surveys, REST API for response management, and self-hosting configuration with TypeScript.
Formik
Build React forms with Formik using useFormik hook, Field components, and schema validation via Yup or Zod. Covers form state management, custom field components, async validation, and TypeScript integration patterns.
Jotform
Integrate JotForm's REST API to create forms, retrieve submissions, process payments, manage widgets, and configure webhooks. Covers form CRUD, submission handling, payment gateway integration, and TypeScript client patterns.
React Hook Form
Build performant React forms with React Hook Form using register, validation, controllers, and form state management. Covers uncontrolled inputs, schema validation with Zod, controlled components, and TypeScript integration.