Skip to main content
Technology & EngineeringForm Survey Services240 lines

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.

Quick Summary22 lines
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 lines
Paste into your CLAUDE.md or agent config

Tally 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 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.

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

Get CLI access →

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.

Form Survey Services246L

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.

Form Survey Services287L

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.

Form Survey Services246L

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.

Form Survey Services279L

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.

Form Survey Services234L

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.

Form Survey Services249L