API keys, requests and errors
Create an API key, send authenticated requests to the REST API, and handle pagination, errors and rate limits.
The REST API lets you read forms and responses, close or reopen a form, and manage webhooks from your own scripts. Everything is JSON over HTTPS.
The base URL is {APP_URL}/api/v1, for example https://formwork.movortech.com/api/v1. Replace the host with your Formwork address. Local development uses http://localhost/forms/api/v1.
Create a key
Open API keys from the account menu at the bottom of the sidebar, or go to /account/api. Enter a Name such as "Zapier" or "nightly export", choose the Access level (Read only or Read and write), and select Create key. Owners and editors can create write keys.
A key:
- belongs to one workspace, the one you were in when you created it, and never reaches another;
- is read (GET requests only) or write (also PATCH, POST and DELETE);
- never gives more than you can do yourself: a viewer's write key still cannot change anything, and viewers can only create read-only keys;
- is shown once, stored only as a SHA-256 hash, and starts with
fwk_; - can be revoked on the same page if it leaks. Requests using it then fail.
You can have up to 10 active keys. Changing your password revokes all of your keys, and leaving a workspace revokes the keys you made in it.
The API is a plan feature. By default read keys need a plan with API read access and write keys need write access. See Default plan limits.
Send a request
Send the key as a bearer token:
export FORMWORK_KEY=fwk_0123456789abcdef0123456789abcdef01234567
curl https://formwork.movortech.com/api/v1/me -H "Authorization: Bearer $FORMWORK_KEY"
There are no cookies, sessions or CSRF tokens on the API. Never put a key in a URL.
GET /me
Who the key acts as.
curl -s https://formwork.movortech.com/api/v1/me -H "Authorization: Bearer $FORMWORK_KEY"
{
"data": {
"user": { "id": 1, "name": "Demo Maker", "email": "demo@formwork.test" },
"workspace": { "id": 1, "name": "Demo Maker's workspace", "role": "owner" },
"key": { "id": 3, "name": "Zapier", "prefix": "fwk_22cd", "scope": "read" }
}
}
Responses and errors
Success wraps the payload in data. Lists add meta:
{ "data": [ ... ], "meta": { "page": 1, "per_page": 25, "total": 135 } }
Errors always look like this, with a matching HTTP status:
{ "error": { "code": "not_found", "message": "Form not found." } }
| Status | code | When |
|---|---|---|
| 400 | bad_request | Malformed request |
| 401 | unauthorized | Missing, wrong or revoked key (also sent with WWW-Authenticate: Bearer) |
| 403 | forbidden | Read-only key on a write route, or your role in the workspace does not allow it |
| 404 | not_found | No such endpoint, or a form or response outside the key's workspace. Both look the same on purpose |
| 422 | validation_failed | Bad parameter or body. error.details.field names the field when there is one |
| 429 | rate_limited | See below. Retry-After is in seconds |
| 500 | server_error | Our side. The message is generic, details go to the server log |
A form or response in another workspace gives the same 404 as one that does not exist, so a key cannot be used to find out what exists elsewhere.
Rate limits
120 requests per minute per key. Failed authentication is limited separately, at 30 per minute per IP address, to make key guessing pointless. Both answer 429 with Retry-After: 60.
When you get a 429, wait the number of seconds in Retry-After and try again, rather than retrying at once.
Pagination
List endpoints take page (from 1) and per_page (1 to 100, default 25) and return meta.total.
curl -s "https://formwork.movortech.com/api/v1/forms?page=2&per_page=50" -H "Authorization: Bearer $FORMWORK_KEY"
Timestamps are ISO 8601 in UTC.
Versioning
This is v1. Additive changes such as new fields and new endpoints can land at any time, so ignore fields you do not know. Anything that breaks existing clients will ship as /api/v2.
Next
Updated Sep 30, 2026