API Authentication
All WittyForm API requests require authentication via an API key. This guide covers how to generate keys, choose the right scopes, and follow security best practices.
Generating API keys
- Log in to your WittyForm dashboard.
- Navigate to API Keys in the sidebar.
- Click Generate new key.
- Enter a descriptive label (e.g., "Production backend" or "Zapier integration").
- Select one or more scopes (see below).
- Click Create.
The full key is displayed only once. Copy it immediately and store it in a secure location such as an environment variable or a secrets manager.
Key scopes
Each API key can be scoped to limit what it can do. Choose the minimum scope needed for your use case:
| Scope | Permissions | Use case |
|---|---|---|
read | List and retrieve forms, responses, and settings | Dashboards, analytics, Zapier triggers |
write | Create and update forms, submit responses | Programmatic form creation, data import |
admin | Full access including delete, webhooks, and account settings | Backend services that need complete control |
A key with write scope also inherits read permissions. A key with admin scope inherits both read and write.
Granular per-resource scopes
The three scopes above (read, write, admin) are coarse: each one acts as a wildcard across every resource. They are kept for backward compatibility, and you can keep using them. For least privilege, a key can instead hold one or more granular resource:action scopes, where:
- resource is one of
forms,responses,webhooks,quizzes. - action is one of
read,write,admin(still hierarchical:admin>write>read).
A granular scope only grants access to its own resource. For example, a key with only responses:read can read responses but cannot touch forms, webhooks, or quizzes. The coarse scopes behave as wildcards in the other direction: a legacy read key satisfies any *:read requirement (such as forms:read or quizzes:read), and a legacy admin key satisfies everything.
| Endpoint group | Read | Write | Delete |
|---|---|---|---|
| Forms | forms:read | forms:write | forms:admin |
| Responses | responses:read | responses:write | responses:admin |
| Quizzes | quizzes:read | quizzes:write | quizzes:admin |
| Webhooks | webhooks:read | webhooks:admin | webhooks:admin |
Scopes (coarse or granular) are chosen when you create the key, and they cannot be widened afterward. To change a key's access, generate a new key with the scopes you need and revoke the old one.
Bearer token format
Include your API key in the Authorization header using the Bearer scheme, or in the X-API-Key header. Either works:
Authorization: Bearer wf_live_abc123def456ghi789
# or, equivalently:
X-API-Key: wf_live_abc123def456ghi789API keys are prefixed with wf_live_. Do not pass the key in the URL as a query parameter (such as ?api_key= or ?key=): such requests are rejected with a 400 response and the error code KEY_INVALID.
Example request with curl
curl -X GET "https://wittyform.com/api/v1/forms" \
-H "Authorization: Bearer wf_live_abc123def456ghi789" \
-H "Content-Type: application/json"A successful response returns 200 OK with { "success": true, "data": { ... } }. Every error uses the same envelope, with a stable code and a human-readable message. An invalid key returns:
{
"success": false,
"error": {
"code": "KEY_INVALID",
"message": "Invalid or expired API key"
}
}Possible error codes:
| Code | HTTP status | Meaning |
|---|---|---|
UNAUTHENTICATED | 401 | No API key was provided |
KEY_INVALID | 401 (400 if passed in the URL) | The key is wrong or revoked, or was supplied as a query parameter |
KEY_EXPIRED | 401 | The key has expired |
INSUFFICIENT_SCOPE | 403 | The key lacks the scope this endpoint requires |
PLAN_REQUIRED | 403 | API access requires a Pro or higher plan |
RESPONSE_LIMIT_REACHED | 403 | The form has reached its response limit |
RATE_LIMITED | 429 | Too many requests; see the Retry-After header |
NOT_FOUND | 404 | The requested resource does not exist |
FORM_NOT_PUBLISHED | 422 | The form is not published, so it cannot accept responses |
FORM_CLOSED | 422 | The form is closed and no longer accepts responses |
VALIDATION_ERROR | 422 | The request body failed validation |
UNSUPPORTED_MEDIA_TYPE | 415 | The request was not sent as application/json |
INTERNAL_ERROR | 500 | An unexpected server error occurred |
Example with JavaScript fetch
const API_KEY = process.env.WITTYFORM_API_KEY;
const response = await fetch('https://wittyform.com/api/v1/forms', {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}
const { data } = await response.json();
console.log(`Found ${data.forms.length} forms`);Example with Python requests
import os
import requests
API_KEY = os.environ["WITTYFORM_API_KEY"]
response = requests.get(
"https://wittyform.com/api/v1/forms",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
)
response.raise_for_status()
data = response.json()["data"]
print(f"Found {len(data['forms'])} forms")Idempotent requests
Any create (POST) request can carry an optional Idempotency-Key header. Send a unique value (a UUID works well) and, if you have to retry the request because the response did not reach you, reuse the same key. The API replays the original response with an Idempotent-Replayed: true header instead of creating a duplicate. Reusing a key with a different request body returns 422. Keys are remembered for 24 hours.
curl -X POST "https://wittyform.com/api/v1/forms" \
-H "Authorization: Bearer wf_live_abc123def456ghi789" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7b9c1d2e-3f40-4a51-8b62-9c0d1e2f3a4b" \
-d '{ "title": "Contact Form" }'See the create endpoints in the Forms API and Responses API for full examples.
Key rotation
To rotate a key without downtime:
- Generate a new key with the same scopes.
- Update your application to use the new key.
- Verify that requests succeed with the new key.
- Revoke the old key (see below).
Both keys are valid simultaneously until the old one is revoked, so there is no interruption during the transition.
Revoking keys
- Go to API Keys in the sidebar.
- Find the key you want to revoke.
- Click the Revoke button and confirm.
Revocation is immediate. Any request using the revoked key will receive a 401 Unauthorized response.
Security best practices
- Never expose keys in client-side code. API keys should only be used in server-side environments. If you need to submit forms from a browser, use the public form URL instead.
- Use the minimum scope. A read-only dashboard only needs the
readscope. - Rotate keys regularly. Set a calendar reminder to rotate production keys every 90 days.
- Use environment variables. Store keys in
.envfiles (excluded from version control) or a secrets manager like AWS Secrets Manager, HashiCorp Vault, or Doppler. - Monitor usage. Each key shows a Last used timestamp on the API Keys page. If you suspect a key is compromised, revoke it immediately.
- Use separate keys per environment. Create distinct keys for development, staging, and production.