Authentication
The AnimationFunnel REST API uses bearer tokens. Generate a key once, scope it appropriately, send it in the Authorization header on every request, and rotate it periodically. This page covers the full lifecycle — creating, scoping, using, testing, rotating, and revoking keys — plus the security pitfalls worth avoiding.
Overview
The AnimationFunnel REST API is the same API the dashboard itself uses. Anything you can do in the UI, you can do from code: create forms, publish snapshots, query submissions, re-score leads, dispatch integrations, manage team members.
Authentication is via bearer tokens — opaque API keys you generate in the dashboard and send on every request. No OAuth dance is required for the common case; OAuth is available on Enterprise for agencies that build on top of AnimationFunnel.
At a glance:
- Auth scheme —
Authorization: Bearer <key> - Protocol — HTTPS only. Plain HTTP is refused.
- Format — JSON request bodies, JSON responses, RFC 7807 error bodies.
- Keys are environment-scoped —
livevs.test. - Keys are permission-scoped — read-only, read-write, admin.
- Keys are optionally IP-allowlisted.
Base URL & versioning
Every API call goes to:
https://api.animationfunnel.com/v1/...The version prefix (/v1) is part of the URL. Breaking changes go into a new version; within a version, only backwards-compatible additions happen. When /v2 ships, /v1 continues to work for at least 24 months with a deprecation warning header.
api.animationfunnel.com with your instance's API hostname. Everything else on this page is identical.API key types
Every key combines three axes: environment, scope, and (optionally) form attachment.
Environment — live vs. test
af_live_...— operates against production data. Real submissions, real integrations, real billing impact.af_test_...— operates against a sandboxed workspace clone. Safe to use in CI and staging — no real submissions are created, no real integrations fire, no billing meters tick.
Scope — read-only, read-write, admin
read— GET requests only. Can read forms, submissions, analytics, integration logs. Cannot mutate anything.read_write— full CRUD on forms, submissions, integrations. Cannot manage billing, API keys, or team members.admin— full workspace control including keys, team, and billing. Treat as dangerous.
Attachment — workspace vs. form-scoped
- Workspace keys — can access every form in the workspace. Default.
- Form-scoped keys (Business+) — restricted to a specific form or folder. Useful for agencies running a single integration per client without risk of cross-access.
Create an API key
- Open
Settings → API keysin your workspace. - Click
New key. - Give it a descriptive name —
production-backend,staging-ci,zapier-import. Names are mandatory because they show up in the audit log; future-you will thank present-you. - Pick an environment (
liveortest). - Pick a scope (
read,read_write,admin). Use the least scope that works. - Optionally restrict to a form or folder (Business+).
- Optionally set an expiration date — the key is auto-revoked on hit.
- Optionally add an IP allowlist (see IP allowlist).
- Hit
Create. The key is shown once — copy it immediately.
Using the key
Send the key in the Authorization header on every request, using the Bearer scheme:
curl https://api.animationfunnel.com/v1/forms \
-H "Authorization: Bearer af_live_..."A successful response
{
"object": "list",
"data": [
{
"id": "frm_01HX...",
"object": "form",
"slug": "paid-search-lead",
"status": "published",
"version": 12,
"created_at": "2026-03-01T10:00:00Z",
"updated_at": "2026-04-15T14:32:11Z"
}
],
"has_more": false
}Authentication errors
Missing, malformed, or invalid keys return 401 Unauthorized:
{
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The API key provided is invalid or has been revoked.",
"request_id": "req_01HZK9..."
}Keys that exist but lack the required scope return 403 Forbidden with insufficient_scope.
Code examples
The same request — GET /v1/forms — across common languages:
Node.js (fetch)
const res = await fetch("https://api.animationfunnel.com/v1/forms", {
headers: {
Authorization: `Bearer ${process.env.ANIMATIONFUNNAL_API_KEY}`,
},
});
if (!res.ok) {
throw new Error(`API error: ${res.status} ${await res.text()}`);
}
const { data } = await res.json();
console.log(`Fetched ${data.length} forms`);Python (requests)
import os
import requests
resp = requests.get(
"https://api.animationfunnel.com/v1/forms",
headers={"Authorization": f"Bearer {os.environ['ANIMATIONFUNNAL_API_KEY']}"},
timeout=10,
)
resp.raise_for_status()
forms = resp.json()["data"]
print(f"Fetched {len(forms)} forms")Go
req, _ := http.NewRequest(
"GET",
"https://api.animationfunnel.com/v1/forms",
nil,
)
req.Header.Set("Authorization", "Bearer "+os.Getenv("ANIMATIONFUNNAL_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// ... decode JSONRuby
require "net/http"
require "json"
uri = URI("https://api.animationfunnel.com/v1/forms")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV['ANIMATIONFUNNAL_API_KEY']}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
raise res.body unless res.is_a?(Net::HTTPSuccess)
puts JSON.parse(res.body)["data"].lengthPHP
<?php
$ch = curl_init("https://api.animationfunnel.com/v1/forms");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . getenv("ANIMATIONFUNNAL_API_KEY"),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new Exception("API error: $status $body");
}
$forms = json_decode($body, true)["data"];Scopes & permissions
Each endpoint documents the minimum scope it requires. A rough map:
GET /v1/forms,GET /v1/submissions,GET /v1/analytics→readPOST /v1/forms,PATCH /v1/forms/{id},POST /v1/submissions/{id}/rescore→read_writePOST /v1/api_keys,DELETE /v1/api_keys/{id},POST /v1/team/invites→admin
Call an endpoint without the needed scope and you'll get a 403 with code: insufficient_scope.
Test mode
Test-mode keys (af_test_...) are the safe way to integrate against AnimationFunnel from staging, CI, or local dev without risk of polluting production data.
- Test keys operate against a sandboxed clone of your workspace with identical form definitions.
- Submissions created in test mode are flagged as
test: trueand are segregated in the submissions view. - Integrations in test mode fire against mock endpoints — no real webhooks, no real Pipedrive deals, no real OpenRouter cost.
- Rate limits in test mode are the same as production so limit-handling code can be exercised.
- You can delete all test-mode data from
Settings → Data & privacy → Reset test data.
IP allowlist
Every key can optionally be restricted to a list of IPv4 or IPv6 addresses (or CIDR ranges). Requests from any other address return 403 Forbidden with code: ip_not_allowed.
Allow list examples:
203.0.113.42 # single IP
203.0.113.0/24 # subnet
2001:db8::/32 # IPv6 subnetParticularly valuable for server-side integrations where the caller's IP is stable. Set from the key's edit drawer in the dashboard.
Error responses
All errors follow RFC 7807 ("Problem Details for HTTP APIs") with a stable JSON shape:
{
"type": "<machine-readable error type>",
"code": "<specific error code>",
"message": "Human-readable description.",
"request_id": "req_01HZK9...",
"details": { /* optional, endpoint-specific */ }
}Error types
authentication_error(401) — missing, invalid, expired, or revoked key.permission_error(403) — valid key with insufficient scope or wrong form attachment.not_found(404) — resource doesn't exist (or isn't visible to this key).validation_error(400 / 422) — request body failed validation.detailslists per-field issues.rate_limit_error(429) — rate limit hit. Honor theRetry-Afterheader.server_error(500–599) — something broke on our side. Retry with backoff.
Using the request ID
Every response (success and error) includes a request_id field and an X-Request-Id header. When contacting support, including the request ID lets us find the exact log entry in seconds. Log it on your side for every request so correlating issues is trivial.
Rate limits
Rate limits apply per key. Every response includes the current limit state as headers:
X-RateLimit-Limit— total requests allowed in the current window.X-RateLimit-Remaining— how many you have left.X-RateLimit-Reset— Unix timestamp of when the window resets.Retry-After— on 429 responses, seconds to wait before retrying.
Default limits: 100 req/s per key, 10k req/min per workspace. Enterprise plans can raise both. See the dedicated rate limits guide for the full policy, including burst behavior.
Webhook signatures vs. API auth
A common point of confusion: the API uses bearer tokens for authentication, but webhooks use HMAC signatures for the opposite direction (AnimationFunnel → your server). These are two different mechanisms:
- API keys — used when you call us. Sent in the
Authorizationheader. Authenticates your identity. - Webhook signatures — used when we call you. Sent in the
X-AnimationFunnel-Signatureheader. Proves our identity and payload integrity.
See the webhooks guide for how to verify a webhook signature. Never use your API key to verify a webhook — they serve different purposes.
OAuth (Enterprise)
For partners building integrations on top of AnimationFunnel — agencies that want to offer form-building to their clients without handling customer API keys — OAuth 2.0 is available on Enterprise plans.
The flow is standard three-legged OAuth with PKCE:
- Partner app redirects the user to
https://animationfunnel.com/oauth/authorize?client_id=...&scope=.... - User grants consent; AnimationFunnel redirects back with a temporary code.
- Partner exchanges the code for an access token (expires) and a refresh token (long-lived).
- Partner calls the API with the access token as a bearer token, exactly like an API key.
Contact Enterprise sales to enroll as an OAuth partner.
Key rotation
Rotate long-lived keys at least once a quarter, and immediately if:
- A laptop with access is lost, stolen, or decommissioned.
- A team member with access leaves.
- A dependency that handled the key is retired or compromised.
- The key is accidentally committed to a repo, pasted into Slack, or logged somewhere indexable.
Zero-downtime rotation
- Create a new key alongside the old one, with the same scope.
- Deploy the new key to every consumer. AnimationFunnel accepts both during overlap.
- Watch the Last usedtimestamp on the old key until it's stale for 24 hours.
- Revoke the old key. The dashboard audit log records who revoked it and when.
Revocation
Click the trash icon next to any key in the API keys list. Revocation is immediate — the next request using that key returns 401 within seconds. Revoked keys cannot be restored; create a new one instead.
Every revocation is written to the workspace audit log with the revoking user, the key name, and a free-text reason prompt. Filling in the reason is optional but recommended — it makes future security reviews dramatically easier.
Security best practices
- Never commit keys to git.Use environment variables, a secrets manager (Doppler, Vault, AWS Secrets Manager), or your platform's built-in secrets (Vercel, Netlify, Heroku config vars). A leaked key is a full breach.
- Use the least scope that works. A script that only reads submissions should not have
admin. Downgrade aggressively. - Never use a live key in client-side code. Bearer tokens belong server-side. If you need the browser to talk to AnimationFunnel, proxy through your own backend so the key never reaches the client.
- Rotate quarterly. Treat keys like passwords. Stale keys are attack surface.
- Use IP allowlists for stable-IP consumers. Server-to-server is usually stable; an allowlist makes key theft nearly useless to an attacker.
- Use test keys in staging. Separate environments prevent staging bugs from touching production.
- Log the request ID on every call.When something goes wrong, you'll want it.
- Audit the keys list monthly.Stale keys (> 90 days unused) should be revoked; keys you don't recognize should be investigated immediately.
- Never email a key. Use a secrets-sharing tool (1Password, Bitwarden Send) or a dedicated onboarding flow.
- Revoke on exit. When a team member leaves, review every key they created and rotate or revoke. The audit log makes this mechanical.
FAQ
How many API keys can I have?
No hard limit. Create as many as you need — one per service, environment, or integration is a clean pattern. The value is in the name, scope, and last-used timestamp; those make audit possible.
I accidentally committed a key. What do I do?
- Revoke the key nowfrom the dashboard. Don't wait to force-push — the key is in the commit history, and crawlers scan public repos in seconds.
- Create a new key and deploy it.
- Rewrite git history to remove the commit, force-push, and invalidate any cached clones.
- Check the key's last-used log entries — if an IP or time looks unfamiliar, your data may have been accessed. Contact support.
Can I share a key with a teammate?
Technically yes; practically no. Shared keys break audit (you can't tell who did what). Instead, add your teammate to the workspace with an appropriate role; they then generate their own key.
I'm transferring a form to another workspace. What happens to keys?
Keys are workspace-scoped. After transfer, the receiving workspace's keys can access the form; the origin workspace's keys can no longer reach it. Form-scoped keys attached to the transferred form are automatically revoked; create new ones in the destination.
How do I debug an auth failure?
- Copy the failing curl command. Run it against a known-good endpoint (
GET /v1/me) to isolate auth from endpoint-specific issues. - Check the
codefield in the response body —invalid_api_keyvs.insufficient_scopevs.ip_not_allowedeach have different causes. - Confirm the key value. A trailing newline from copy-paste is the most common culprit.
- Confirm the
Authorization: Bearerprefix. The scheme name is case-sensitive. - Log the
X-Request-Idheader and send it to support if the above doesn't reveal the issue.
Next steps
Was this page helpful?