REST API
REST API
Read your forms and completed responses over HTTPS, and subscribe to new ones. Every request carries a workspace API key.
All endpoints live under one base URL and return JSON. The full list is in the API reference.
https://api.enform.co/api/v1Get an API key
- In Enform, open Settings > API and create a key. API keys are part of Pro.
- Only the workspace owner can create and revoke keys. A workspace can have up to 20 at a time.
- You see the key once, when you create it. Store it somewhere safe.
- A key belongs to one workspace and reads every form in it.
- Revoking a key also removes the webhooks it created.
Authenticate
Send the key as a bearer token on every request. GET /me is a quick way to check that a key works and see which workspace it belongs to.
curl "https://api.enform.co/api/v1/me" \
-H "Authorization: Bearer $ENFORM_API_KEY"const res = await fetch("https://api.enform.co/api/v1/me", {
headers: {
Authorization: "Bearer " + process.env.ENFORM_API_KEY,
},
});
const json = await res.json();import os, requests
res = requests.get(
"https://api.enform.co/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['ENFORM_API_KEY']}"},
)
data = res.json(){
"type": "account",
"data": {
"workspace": {
"id": "Hq7cT2vLm9xR4bWn",
"name": "Acme"
}
}
}Responses and lists
Every response is wrapped as { type, data }. Lists add hasMore and nextCursor, newest first.
{
"type": "list",
"data": [
{
"id": "c3a1f7e2-1b4d-4e8a-9c2f-6d5e4b3a2f10",
"title": "Customer feedback",
"published": true,
"createdAt": "2026-10-28T14:02:11.000Z",
"updatedAt": "2026-11-02T08:40:53.000Z"
}
],
"hasMore": true,
"nextCursor": "eyJ0IjoiMjAyNi0xMC0yOFQxNDowMjoxMS4wMDBaIiwiaWQiOiJjM2ExIn0"
}Pagination
limitinteger- How many items to return, from 1 to 100. Defaults to 25.
cursorstring- The
nextCursorfrom the previous page. Leave it out for the first page. sinceISO 8601- Responses only. Returns responses completed after this time, for example
2026-11-01T00:00:00Z.
Keep asking while hasMore is true. An empty cursor or since counts as not set.
let cursor = null;
const all = [];
do {
const url = new URL("https://api.enform.co/api/v1/forms/c3a1f7e2-1b4d-4e8a-9c2f-6d5e4b3a2f10/submissions");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.ENFORM_API_KEY}` },
});
const page = await res.json();
all.push(...page.data);
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);Field keys
Every question has an id, a key, a label and a type. The key is the question's stable ID, the same value as id. It never changes when you rename or reword a question, so map answers by key, never by label. The label is the current question text, for display only.
Answers to questions you later delete from the form keep their original ID as the key.
How answers read
A response gives you each answer two ways:
answersmaps each question's key to a readable text version of the answer.fieldslists every answer in form order with itskey,label,type, the rawvalueand the same readabletext.
{
"type": "submission",
"data": {
"id": "5b0e4c1a-7d2f-4f0e-9a51-2f6c8d0b9e13",
"formId": "c3a1f7e2-1b4d-4e8a-9c2f-6d5e4b3a2f10",
"createdAt": "2026-11-03T09:14:02.511Z",
"completedAt": "2026-11-03T09:16:40.020Z",
"answers": {
"bba1826f-56fc-4e44-a0fa-bac06310da49": "Ada Lovelace",
"4dfa4a9e-85e3-4187-b38e-ae1d4675f99b": "9",
"a2d90fd0-3942-45c8-ade3-773ed5f9733e": "Weekly reports, Slack alerts",
"e7c2a9d4-0b3f-4c1e-9a8d-5f6b7c8d9e01": "ada@example.com"
},
"hidden": {
"email": "ada@example.com"
},
"fields": [
{
"id": "bba1826f-56fc-4e44-a0fa-bac06310da49",
"key": "bba1826f-56fc-4e44-a0fa-bac06310da49",
"label": "Your name",
"type": "short_text",
"value": "Ada Lovelace",
"text": "Ada Lovelace"
},
{
"id": "4dfa4a9e-85e3-4187-b38e-ae1d4675f99b",
"key": "4dfa4a9e-85e3-4187-b38e-ae1d4675f99b",
"label": "How likely are you to recommend us?",
"type": "opinion_scale",
"value": 9,
"text": "9"
},
{
"id": "a2d90fd0-3942-45c8-ade3-773ed5f9733e",
"key": "a2d90fd0-3942-45c8-ade3-773ed5f9733e",
"label": "What should we build next?",
"type": "checkbox",
"value": [
"Weekly reports",
"Slack alerts"
],
"text": "Weekly reports, Slack alerts"
},
{
"id": "e7c2a9d4-0b3f-4c1e-9a8d-5f6b7c8d9e01",
"key": "e7c2a9d4-0b3f-4c1e-9a8d-5f6b7c8d9e01",
"label": "email",
"type": "hidden",
"value": "ada@example.com",
"text": "ada@example.com"
}
]
}
}Extra maps, when the form has them
hiddenobject- Values passed in the form's link, by hidden field name. See hidden fields.
calculationsobject- Totals the form worked out, by variable, such as
score. paymentsobject- Each payment answer by question ID:
status,amountandamountRefundedin the currency's smallest unit (cents for USD),currency,paymentIntentId,chargeIdandlivemode.
Answer text by question type
- Yes/no answers read
YesorNo. Multiple choices are joined with commas. - Grid (matrix) answers read row by row:
Speed: Good; Price: Fair. - Ranking answers read in order:
1. Slack alerts, 2. Weekly reports. - File uploads give the file's link.
- Payments read like
$25.00 paid. Status can be pending, succeeded, failed, canceled, refunded or partially refunded.
{
"answers": {
"calc:score": "12",
"b41c6e2a-8d7f-4a3b-9c1e-2d5f6a7b8c90": "Speed: Good; Price: Fair",
"f0e2d1c3-5b4a-4f6e-8d7c-9a0b1c2d3e4f": "1. Slack alerts, 2. Weekly reports",
"77ad3b9c-1e2f-4a5b-8c6d-7e8f9a0b1c2d": "$25.00 paid"
},
"calculations": {
"score": 12
},
"payments": {
"77ad3b9c-1e2f-4a5b-8c6d-7e8f9a0b1c2d": {
"status": "succeeded",
"amount": 2500,
"currency": "usd",
"amountRefunded": 0,
"paymentIntentId": "pi_3QxYz8Lk2eZvKYlo0a1b2c3d",
"chargeId": "ch_3QxYz8Lk2eZvKYlo0d4e5f6g",
"livemode": true
}
}
}Rate limits
- Each key can make 120 requests a minute to each endpoint.
- Each IP address can make 1,200 requests a minute to each endpoint. This also covers requests with a missing or wrong key.
- Over the limit you get a
429. Wait a moment and try again.
Errors
Errors use the HTTP status code and a JSON body that says what went wrong.
{
"success": false,
"error": "Form not found.",
"status": 404
}400Bad request- A parameter or body field is wrong, such as a limit over 100, a bad cursor or a webhook URL that isn't https.
401Unauthorized- The key is missing, wrong or revoked, or the person who made it no longer owns the workspace.
403Forbidden- The workspace's plan doesn't include API access.
404Not found- The form, response or webhook doesn't exist in this workspace.
429Too many requests- You went over a rate limit.
OpenAPI
The machine-readable spec is at https://api.enform.co/api/v1/openapi.json. Import it into Postman, Insomnia or a code generator. The plain Swagger view is still at https://api.enform.co/api/v1/docs.