All docs

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.

Base URL
https://api.enform.co/api/v1

Get 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"
Response
{
  "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.

GET /forms
{
  "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 nextCursor from 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.

Read every response
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:

  • answers maps each question's key to a readable text version of the answer.
  • fields lists every answer in form order with its key, label, type, the raw value and the same readable text.
GET /submissions/{submissionId}
{
  "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, amount and amountRefunded in the currency's smallest unit (cents for USD), currency, paymentIntentId, chargeId and livemode.

Answer text by question type

  • Yes/no answers read Yes or No. 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.
Calculations, grids, rankings and payments
{
  "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.

404
{
  "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.

What do you want to ask?

Start free

Free, no card needed.