tally
tally.
Tally forms — list, read, author, and delete them.
setup & usage
create a key on tally.so/settings/api-keys (Settings → API keys → Create API key), then paste it into oto. It is only shown once.
tly-…, sent as Authorization: Bearersix tools, verb in op:
tally_submission(op="list", form_id="…", after_id="<last id seen>")tally_submission(op="get", form_id="…", submission_id="…")tally_form(op="list"), then op="get" / op="questions" / op="blocks"tally_form(op="create"|"update"|"update_blocks", blocks=[…]) (39 block types, see blocks-reference)tally_analytics(op="drop_off", form_id="…", period="30d")tally_workspace(op="list"|"folders"|…)tally_account(op="me") (it returns organizationId) then op="users"|"invite"|"invites"tally_webhook(op="create", form_id="…", url="https://…", event_types=["FORM_RESPONSE"], signing_secret="…")questions once per page and each response points into it by questionId. tally_submission does the join — each response carries answers: [{question_id, title, type, answer, formatted}], plus answers_by_title only if the titles are unique on that form (otherwise the key is absent and title_collisions says which ones step on each other). raw=True returns the Tally payload intact.[{id, name, url, mimeType, size}], verified live. The three metadata fields name/mimeType/size arrive IN the response: enough to check that a file is present and in the right format without downloading anything (which makes a completeness check possible even before a DPA allows reading the content). Each response also carries pdf_url and preview_url — Tally renders the full response as a PDF, no need to rebuild it.accessToken = a JWT, plus signature, in the query string): preview_url, pdf_url and the url of each file. The token IS the access right — these are not public links. They travel through the agent's context and through anything that logs a tool result: treat them as a bearer of rights, not as an inert reference.answer is not already readable (a choice question, whose answer is an option id) — unverified. answers_by_title therefore falls back to answer, and formatted can be null.formattedAnswer. The client therefore always sends tally-version (default 2026-08-04).formId, url, eventTypes, isEnabled all required). tally_webhook(op="update") re-reads the webhook and merges — do not bypass it by hitting the API by hand.op="update" and op="update_blocks": a block absent from the array is deleted. Read the state with op="blocks" first.tally_webhook registers a URL that YOU control (n8n, Make, your service); to bring responses into oto, schedule a tally_submission(op="list", after_id=…). Webhook deliveries do not consume the 100 req/min quota — polling does.four operations destroy something; all accept dry_run=True, which validates identically and returns a real diff instead of writing:
tally_submission(op="delete") — irreversible: Tally documents a trash for forms and workspaces, not for responsestally_account(op="remove_user") — removes someone from the organization and revokes every API key they created, possibly including the one of the current calltally_workspace(op="delete") and op="delete_folder" — take the contained forms with them (trash, restorable)tally_form(op="delete") — trash, restorablethis is the costliest trap of this API, recorded live:
GET /webhooks returns 401 as long as no webhook has ever been created on the account. After a first creation it returns 200 — and keeps doing so even after all of them are deleted. The 401 says "the webhooks integration does not exist yet", not "your key is bad".tally_form(op="blocks"), tally_form(op="update_question") and tally_workspace(op="create") return 401 on a FREE plan: it is a PLAN gate, not authentication.the connector therefore translates no 401 into "key rejected" — even without known context, the message names both causes and points to tally_account(op="me"), which settles it: if it answers, the key is good and it is the plan that blocks.
derived from the real OpenAPI 3.0.1 spec (developers.tally.so/api-reference/openapi.json, read on 2026-08-31), and tested live on 2026-08-31 with a real tly- key on a FREE account.
exercised for real, conforms to the code: op="me"; the full cycle of a form (create → read → rename → publish → trash); questions; responses (list + filter=partial); the five analytics views; the full cycle of a webhook (create → list → event log → merged PATCH → delete) — including the check that op="update" with only is_enabled=False does not wipe the URL; the refusal of an irrelevant argument; and the fact that dry_run writes nothing and never echoes the signing_secret. The account was returned to its initial state (0 forms, 0 webhooks).
envelopes recorded (the spec did not settle them): /forms and /workspaces return {items, page, limit, total, hasMore}; /webhooks returns {webhooks, …} and not items; /forms/{id}/questions returns {questions, hasResponses}; /organizations/{id}/users and .../invites return a bare array; webhook PATCH/DELETE return an empty body.
NOT exercised, for lack of plan or material, and therefore derived from the spec alone: reading/writing blocks (plan 401), writing workspaces and anything touching folders (Pro), update_question (a freshly created form returns no question, even published), reading and deleting ONE response (no response on the account), invitations (would send real emails), removing a member (would remove the owner from their own org and revoke the call's key), replaying an event (no event delivered).
a question takes its title from the `LABEL` block that precedes it: a form whose inputs have no LABEL returns {"questions": [], "hasResponses": false} even published — it is not a bug, there is simply nothing to name. Found live after believing an endpoint was broken.
⚠️ `metrics` and `drop_off` do not give the same `completionRate` over the same period (100 vs 50 on one visit and one submission, measured): they do not count the same thing. Do not mix them in one dashboard without saying which one is cited.
block traps met at creation: a TITLE or LABEL block must not share its groupUuid with an input block, and TitlePayload carries html, not title (only FormTitlePayload has both).
this connector accepts multiple comptes: each stored credential becomes a named account (one name per compte), at your level, your team's or your org's.
_account="<name>" on the tool; list them: oto_identity(op='list', connector='tally') (scope='org' or scope='group' for the org's or team's)oto_identity(op='set', connector='tally', identity_id='<name>'); rename: op='rename' with new_name_account) and adding upoutils
utilisation
claude plugin marketplace add otomata-tech/oto-plugin — mcp + skill configurés.pipx install oto-cli puis oto tally …