The REST API
Create and update forms, list and delete submissions and manage webhooks over a bearer-key REST API served by your own install.
The REST API gives a script the same reach over your forms that you have in the app. Create a form for each new client, keep a dropdown's options in step with a live source, pull submissions into a warehouse, wire a webhook from a deploy script. Every install serves it under its own address, at /api/v1, and the reference for each endpoint lives on that install at /developers.

Developer docs
The endpoint reference is served by Tinyform itself at /developers, and it is generated from the same code that answers the requests, so it cannot describe a route that is not there. The rail in the app, the API keys page and the help bubble all link to it.
API basics
- Base URL: your own Tinyform address plus
/api/v1, for examplehttps://forms.example.com/api/v1. There is no separate API host. - Authentication: create a key under
Settings, thenAPI keys, withCreate API key. Give it a name; the key, which starts withfk-, is shown once with a copy button. Only a hash of it is stored, so a lost key is deleted and replaced, never recovered. Send it on every request asAuthorization: Bearer fk-.... - Who a key is: the person who created it. It sees their workspaces and their forms, through the same membership checks the app uses.
- Rate limit: 100 requests per minute per key, and 300 per minute per client address. Past either, the answer is
429with aRetry-Afterheader. Use a webhook rather than polling for new submissions. - Versioning: there is no version header.
/developersalways describes what the API serves today, so it is the reference to read rather than a version number to pick. - Paging: list endpoints return an envelope with
items,page,limitandhasMore. Pass?page(starting at 1) and?limit(default 50, at most 500) and keep going whilehasMoreis true. - Errors: a JSON body of the shape
error: "..."with the matching status. Every path under/api/v1that does not exist, in any method, answers a JSON404, never the app's HTML not-found page.
| Code | Meaning |
|---|---|
| 200 | The request succeeded |
| 401 | The key is missing, malformed or deleted |
| 403 | The change is refused for this workspace, for instance branding: false without the entitlement |
| 404 | Not found, or not yours: the two are deliberately the same answer, so the API never confirms that a form exists |
| 429 | Rate limited; wait for the Retry-After seconds |
| 500 | Something failed at the other end; the response says no more than that |
Forms in the Trash are invisible on this surface. A deleted form is a 404 until it is restored in the app.
Endpoints
| Method and path | What it does |
|---|---|
GET /api/v1/users/me | The person the key acts as |
GET /api/v1/workspaces | The workspaces they belong to |
GET /api/v1/workspaces/:id | One workspace |
GET /api/v1/forms | Their forms, newest first; ?workspaceId= narrows it |
POST /api/v1/forms | Create a draft from workspaceId, an optional name and an optional document |
GET /api/v1/forms/:id | One form, with a status of draft, published or closed |
PATCH /api/v1/forms/:id | Update name, document or settings; publishing and closing happen in the app |
DELETE /api/v1/forms/:id | Move the form to the Trash |
GET /api/v1/forms/:id/questions | The columns the Submissions table draws, with each one's label, type and key |
GET /api/v1/forms/:id/submissions | Responses, ?filter=all, completed or partial |
GET /api/v1/forms/:id/submissions/:sid | One response |
DELETE /api/v1/forms/:id/submissions/:sid | Delete one response |
GET /api/v1/webhooks | One form's webhooks; ?formId= is required |
POST /api/v1/webhooks | Add a webhook to a form |
PATCH /api/v1/webhooks/:id | Change its URL, secret, headers or enabled state |
DELETE /api/v1/webhooks/:id | Remove it |
GET /api/v1/webhooks/:id/deliveries | Delivery receipts: status, attempts, lastError, submissionId, eventType and dates |
POST /api/v1/webhooks/:id/deliveries/:did/retry | Send one delivery again |
questions and a submission's answers share one vocabulary: label is the column's name as the table heads it (the field name where you renamed one, otherwise the question), key is where the answer sits in the submission, and the list includes a named hidden field, the respondent's country and each calculated field. A FILE or SIGNATURE answer also carries files, a list of name and url pairs where url is a signed link to the attachment, valid for seven days.
What people build with it
Create and personalise forms
Fetch a form you use as a template with GET /api/v1/forms/:id, change what needs changing in its document, and POST /api/v1/forms with the workspace id and the new document to create a draft for a new client. Publish it from the editor, or leave it as a draft for someone to finish.
curl -X POST https://forms.example.com/api/v1/forms \
-H "Authorization: Bearer fk-..." \
-H "Content-Type: application/json" \
-d '{ "workspaceId": "ws_...", "name": "Client intake - Acme Co", "document": { ...copied from GET /api/v1/forms/:id... } }'
Keep a form in step with live data
Read the form, change the option list of a dropdown in its document, and send the whole document back with PATCH /api/v1/forms/:id. The document is replaced as a unit, so always start from a fresh GET rather than a copy you kept earlier, or a block edited in the app since then is lost.
curl https://forms.example.com/api/v1/forms/aB3dE4fG \
-H "Authorization: Bearer fk-..."
curl -X PATCH https://forms.example.com/api/v1/forms/aB3dE4fG \
-H "Authorization: Bearer fk-..." \
-H "Content-Type: application/json" \
-d '{ "document": { ...the full document, with the options updated... } }'
PATCH also takes settings, and it applies the same rules the Settings tab does: asking for branding: false in a workspace without the entitlement is refused with 403.
Read and manage submissions
List a form's responses and forward them downstream, filtering to completed ones only:
curl "https://forms.example.com/api/v1/forms/aB3dE4fG/submissions?filter=completed&limit=100" \
-H "Authorization: Bearer fk-..."
Or skip polling: add a webhook and each completed submission is pushed to your endpoint the moment it is stored, without counting against the rate limit.
Delete a response you no longer need:
curl -X DELETE https://forms.example.com/api/v1/forms/aB3dE4fG/submissions/k3Jd8sPq2 \
-H "Authorization: Bearer fk-..."
The answer is 204. The row and every file attached to it are removed together, and the form's response count goes down by one, the same outcome as the Submissions table's bin.
Frequently asked questions
How do I create an API key?
Open Settings, then API keys, and click Create API key. Name it, copy the key from the confirmation, and keep it somewhere safe: it is not shown again. Deleting a key on that page revokes it immediately.
Is the API free?
Yes. It is part of every install, and nothing on it is gated by plan. The one plan-aware rule is the branding setting noted above, which the API enforces exactly as the app does.
What can I build with it?
Anything that creates or edits forms, reads or deletes submissions, or manages webhooks: a client portal that provisions a form per customer, an onboarding flow, a sync into your own database, an internal dashboard. What it does not do is publish or close a form; those stay in the app.
Can I use it with an automation tool?
Yes, as long as the tool can make HTTP requests with a bearer header. For reacting to new submissions, though, a webhook is the better trigger: it needs no key and no polling.
How do I authenticate?
With a bearer token: Authorization: Bearer fk-.... A request without a valid key is answered 401.
Do I get webhooks or an API?
Both, and they are managed from the same place. Webhooks push a submission to you as it arrives; the API lets you read and change everything at your own pace, including the webhooks themselves.
Can I prefill a form's answers over the API?
Prefilling is done on the published link rather than in the API: see Pre-populate form fields and Hidden fields. What the API can do is create or update the form the link points at.
Is there a rate limit?
Yes: 100 requests per minute per key and 300 per minute per client address, answered with 429 and a Retry-After header when exceeded. Use a webhook for anything that would otherwise poll.
How do I keep an integration stable?
Read /developers again after an upgrade: it always describes what the API serves today. There is no per-request version header to select an older behaviour.