FounderTally
API
Read your account from your own tools, and get your reports and alerts as signed webhooks in Zapier, Make, n8n or your own server. Every number is read live from the services you connected; nothing is cached. Version v1: it only gains fields and endpoints; a breaking change would be a new version.
Keys
In FounderTally, open Settings → API, name a key after the tool that will use it, pick Read or Read and write, and copy it: it is shown once. Send it on every request:
Authorization: Bearer ft_live_...A key reaches its own account only. Only an owner or admin makes or revokes keys; a revoked key stops working at once. At most 600 requests a minute per key: past that the answer is 429 with a Retry-After header. Keys work only under /api/v1.
Endpoints
All under https://foundertally.com/api/v1.
GET /healthThe API answers. No key needed.GET /accountThe account your key belongs to, and the key's scope.GET /sectionsEvery section, whether it is on in your cockpit, and its live status.GET /sections/{id}One section's live numbers, with units and sources.GET /conversationsYour customer-conversation log, newest first.POST /conversationsLog a conversation (a read and write key).
curl https://foundertally.com/api/v1/account \
-H "Authorization: Bearer $FOUNDERTALLY_KEY"curl "https://foundertally.com/api/v1/sections?period=weekly" \
-H "Authorization: Bearer $FOUNDERTALLY_KEY"curl "https://foundertally.com/api/v1/sections/revenue.headline?period=monthly" \
-H "Authorization: Bearer $FOUNDERTALLY_KEY"curl -X POST https://foundertally.com/api/v1/conversations \
-H "Authorization: Bearer $FOUNDERTALLY_KEY" \
-H "content-type: application/json" \
-d '{"who":"Ada","company":"Acme","talkedOn":"2026-11-15","outcome":"interested","nextStep":"Send the trial link"}'period is daily (last 24 hours), weekly (last 7 days), monthly (last 30 days, the default) or all. Numbers come with a unit: count, ratio (0 to 1) or cents with a currency. A section that cannot be read says why in status and reason; a missing number is null, never 0.
Section ids: awareness.visitors, awareness.visitors-by-week, awareness.top-sources, acquisition.new-people, acquisition.signups-by-week, activation.activated, retention.returning, retention.customers-kept, revenue.headline, revenue.mrr-by-week, referral.referred, referral.top-referrers.
Errors
{ "error": { "code": "unauthorized", "message": "This API key is not valid. ..." } }Codes: unauthorized (401), insufficient_scope and forbidden (403), not_found (404), invalid_request (400), rate_limited (429), unavailable (503).
Webhooks
In Settings → Integrations → Notifications, connect Webhook URL with the address your tool gives you (Zapier: Webhooks by Zapier → Catch Hook; Make: Custom webhook; n8n: Webhook node). FounderTally sends a signed test event to check it, then shows its signing secret once. Reports and alerts then arrive as JSON; Send test event sends one any time. One channel at a time: connecting a webhook replaces Slack, Discord or email.
{
"id": "evt_6f1c2a9b0d3e4f5a6b7c8d9e",
"type": "report",
"apiVersion": "v1",
"created": 1763193600,
"account": { "id": "a-3f9c1b2d4e5f6a7b" },
"data": {
"title": "Weekly report",
"text": "MRR $12,400, up 6% ...",
"link": { "label": "Open the report", "url": "https://foundertally.com/dashboard/business/weekly" }
}
}type is report, alert or test. Each request carries X-FounderTally-Event (the type), X-FounderTally-Delivery (the event id: drop a repeat) and X-FounderTally-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of <t>.<raw body> under your signing secret. Answer with a 2xx within 8 seconds. A timeout, a 429 or a 5xx is tried again, 3 tries in all, after 1 s and 4 s; redirects are not followed.
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: the whsec_... value shown once when you connected the webhook.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Questions: support@foundertally.com