Skip to main content
Technology & EngineeringForm Survey Services241 lines

Tally

Integrate Tally forms via its API and embed SDK to handle submissions, configure

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: 241 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 →