FormRead Web API Integration Developers Guide

How the FormRead API Works

The FormRead API is split into two parts: a REST API for managing forms (creating, updating, deleting, and storing data), and an embeddable iframe for form editing and reading scanned documents. The API alone does not process or read forms — reading is handled entirely through the iframe.

  • REST API (this page): Use it to create, update, delete, and retrieve form metadata. The API stores your form data and returns tokens needed to embed the iframe.
  • Iframe (embedded viewer): Use it to edit form templates, upload scanned documents, and extract results (OMR, OCR, BCR). All form reading and data extraction happens inside the iframe.
Looking for Server-to-Server Processing?

If you don't need a visual interface and just want to send images and get results back as JSON, check the Server-Side Processing API.

Typical Integration Workflow
  1. Authenticate with a Bearer token (generated from the dashboard).
  2. Use the POST /api/forms endpoint to create a form and get the iframe_token.
  3. Embed the iframe in your page using the iframe_token to let users edit forms and scan documents.
  4. Listen to iframe events (editForm, getResults, rowResult) to receive form data and extracted results, then store them in your system.

Authentication

For enterprise uses the API can be used by generating and Bearer Auth token.

Login in the FormRead dashboard and access the API Token option:

FormRead dashboard menu with the API Tokens option highlighted

Create the API tokens to allow third-party services to authenticate with our application on your behalf:

Creating a new FormRead API token with permission checkboxes

Copy the token generated in a secure place (it will only be shown once):

The generated FormRead API Bearer token shown once — copy and store it securely

Create New Form

POST /api/forms

Headers

Name Value
Authorization Bearer YOUR_API_TOKEN
Accept application/json

Request

curl --location --request POST 'https://formread.org/api/forms' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer YOUR_API_TOKEN' \
  --form 'form_name="new form"' \
  --form 'custom_css="{ingest custom css to edit FormRead View}"'

Response

{
  "id": 8,
  "form_name": "new form",
  "iframe_token": "6ifvCyEcjB3D5SGqtFmJ5eg0sPYNQfLoWCEgv7bb",
  "created_at": "2022-02-28T21:22:55.000000Z",
  "updated_at": "2022-02-28T21:22:55.000000Z"
}

Variable custom_css will allow to ingest css rules to edit FormRead look and add custom text, like eg:

#app-title::after{
  content: 'Your Tittle';
  color: white;
}
#app-subtitle::after{
  content: 'Your Custom sub-tittle Your Custom sub-tittle Your Custom sub-tittle';
  color: white;
}
#upload-from-cam{
  display: none;
}
#upload-from-system{
  background: lightblue;
}
#upload-from-csv{
  display: none;
}
#app > div.bg-gray-50.h-screen.overflow-auto > div > div{
  background-color: black;
}

Get Form by Id

GET /api/forms/{form_id}

Request

curl --location --request GET 'https://formread.org/api/forms/8' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer YOUR_API_TOKEN'

Response

{
  "id": 8,
  "form_name": "adsf",
  "iframe_token": "po75a0pqut4fa16XWgdEP2qRDwnhgfiqP3H0Dij6",
  "created_at": "2022-02-28T21:22:55.000000Z",
  "updated_at": "2022-03-01T01:40:19.000000Z"
}

Edit a Form

After a form is Created or GET it can be displayed in an Iframe using the iframe_token provided in the response (this token variates so make sure you GET your form before rendering the iframe)

Create also a script that listen to the events of the iframe like in the example below:

<!DOCTYPE html>
<html style="height: 100%">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body style="height: 100%">

<iframe src="https://formread.org/{lang}/api/forms/{form_id}/edit/{iframe_token}/{read_only}"
        style="height: 100%; width: 100%"></iframe>

<script>
    window.addEventListener('message', function (e) {
        // if (e.origin !== 'https://formread.org') return;

        console.log(e.data.method)

        if (e.data.method === "editForm") {
            let formData = e.data.formData // data used to saved your form
            let schema = JSON.parse(e.data.schema)
            console.log(schema)
            console.log(formData)
        }
        if (e.data.method === "getResults") {
            let results = JSON.parse(e.data.results)
            console.log(results)
        }
        if (e.data.method === "rowResult") {
            let rowResult = JSON.parse(e.data.rowResult)
            console.log(rowResult)
        }
    });
</script>
</body>
</html>

Iframe URL Parameters

Parameter Description
lang Set lang variable to display the app commands in the desired language. Currently, we support english (en) , french (fr), spanish (es), brazilian portuguese (pt) or kazakh (kk)
read_only Variable read_only can be set to 1 for disabling form editing or 0 to allow form editing

Iframe Events

When users click on the save button, the <code class="language-plaintext">editForm</code> method will be triggered, there you have access to 2 variables:

  • formData variable will contain the encoded form attributes that can be sent using the Updateto save the changes made to your form:
  • the schema variable will let you know the fields created so far:
    {
      "file_name": {
        "type": "text"
      },
      "BCR-0": {
        "type": "text"
      },
      "OCR-1": {
        "type": "text"
      },
      "OMR-2-0": {
        "type": "select",
        "options": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
        "questionIndex": "0"
      },
      "OMR-2-1": {
        "type": "select",
        "options": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
        "questionIndex": "1"
      },
      "OMR-2-2": {
        "type": "select",
        "options": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
        "questionIndex": "2"
      }
    }

    The schema variable is meant only for you to display some alerts to your users in case you are requiring mandatory area fields to be created:

When a user click on download results, the getResults method will be triggered, there you`ll get the result data in a JSON format so you can store then in your system

Also, you can get the row by row results with the rowResult method, that will be triggered each time a page is processed

Update Form

PUT /api/forms/{form_id}

Request

curl --location --request POST 'https://formread.org/api/forms/8?_method=PUT' \
  --header 'Authorization: Bearer YOUR_API_TOKEN' \
  --form 'form_data="eyJ2dWV4X3N0YXRlIjp7ImZvcm1OYW1lIjoiYWRzZiIsImZvcm1zIjp7f
                     Swic2VsZWN0ZWRGb3JtSWQiOiIiLCJmb3JtUmVhZEFyZWFzIjp7IkJDUi
                     0wIjp7ImNvbHVtblBvc2l0aW9uIjoxLCJ3aWR0aCI6MC4xMTQ5NTA0NTM
                     0MDUzNTE2NCwiaGVpZ2h0IjowL...' \
  --form 'custom_css="{ingest custom css to edit FormRead View}"'

Response

{
  "id": 8,
  "form_name": "adsf",
  "iframe_token": "po75a0pqut4fa16XWgdEP2qRDwnhgfiqP3H0Dij6",
  "created_at": "2022-02-28T21:22:55.000000Z",
  "updated_at": "2022-03-01T02:30:14.000000Z"
}

Delete Form

DELETE /api/forms/{form_id}

Request

curl --location --request DELETE 'https://formread.org/api/forms/8' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer YOUR_API_TOKEN'

Response

1

Complete Example: Embedding the Grading Workflow

Here is an end-to-end example showing how a teacher can design a form, scan exams, and push the graded results to your own backend — all from a single page that embeds the FormRead iframe.

Scenario

A teacher opens your app, designs a "Math 101 - Midterm" answer sheet, prints it, scans the filled-in exams, and clicks "Download results". Your page listens for the iframe events, saves the layout, and posts the graded answers to your grading system.

Step 1 — Create the form from your server

On your backend, create a form for the teacher. You get back an id and an iframe_token — these are what you will pass to the iframe URL in the next step.

# From YOUR backend (never from the browser)
curl --location --request POST 'https://formread.org/api/forms' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer YOUR_API_TOKEN' \
  --form 'form_name="Math 101 - Midterm"'
{
  "id": 42,
  "form_name": "Math 101 - Midterm",
  "iframe_token": "6ifvCyEcjB3D5SGqtFmJ5eg0sPYNQfLoWCEgv7bb",
  "created_at": "2026-04-20T10:00:00.000000Z",
  "updated_at": "2026-04-20T10:00:00.000000Z"
}

Save the id and iframe_token against the teacher's record in your own database. You will use them to render the editable iframe in step 2.

Step 2 — Open the iframe in editable mode

Your backend renders a page containing the FormRead iframe with read_only=0, so the teacher can draw OMR bubbles, OCR areas, and barcodes on the sheet. When they click "Save" inside the iframe, the editForm event fires — the browser forwards the raw formData to your own backend. The browser never calls FormRead directly.

<!-- /design-form page rendered by YOUR backend.
     read_only = 0 → editable mode -->
<iframe id="formread-frame"
        src="https://formread.org/en/api/forms/42/edit/6ifvCyEcjB3D5SGqtFmJ5eg0sPYNQfLoWCEgv7bb/0"
        style="width:100%; height:100vh; border:0"></iframe>

<script>
    // The browser ONLY talks to your own backend. Never to FormRead.
    const FORM_ID = 42;

    window.addEventListener('message', async function (e) {
        if (e.origin !== 'https://formread.org') return;

        // Teacher clicked "Save" inside the iframe
        if (e.data.method === 'editForm') {
            await fetch(`/api/forms/${FORM_ID}/layout`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ form_data: e.data.formData })
            });
        }
    });
</script>

Step 3 — Persist the layout from your backend

Your backend receives the formData string and forwards it to FormRead with a PUT, using your Bearer token (which never leaves the server). This is what permanently saves the form configuration so the same form can later be reused to scan exams.

# Inside your backend handler for POST /api/forms/42/layout
# $formData is the raw string your browser posted.
curl --location --request POST 'https://formread.org/api/forms/42?_method=PUT' \
  --header 'Authorization: Bearer YOUR_API_TOKEN' \
  --form "form_data=${formData}"
{
  "id": 42,
  "form_name": "Math 101 - Midterm",
  "iframe_token": "po75a0pqut4fa16XWgdEP2qRDwnhgfiqP3H0Dij6",
  "created_at": "2026-04-20T10:00:00.000000Z",
  "updated_at": "2026-04-20T10:15:42.000000Z"
}
The iframe_token rotates

Each update to a form returns a new iframe_token. Always fetch the latest one from your backend right before rendering the iframe — do not hard-code or cache it long-term.

Step 4 — Fetch the current iframe_token

Later, when the teacher is ready to scan exams, your backend reads the form by id to get the latest iframe_token.

# From YOUR backend, right before rendering the scan page
curl --location --request GET 'https://formread.org/api/forms/42' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer YOUR_API_TOKEN'
{
  "id": 42,
  "form_name": "Math 101 - Midterm",
  "iframe_token": "po75a0pqut4fa16XWgdEP2qRDwnhgfiqP3H0Dij6",
  "created_at": "2026-04-20T10:00:00.000000Z",
  "updated_at": "2026-04-20T10:15:42.000000Z"
}

Step 5 — Reopen the iframe in read-only mode to scan

Render the iframe again — this time with read_only=1 at the end of the URL. The teacher can no longer edit the layout, only upload scans. The browser forwards rowResult (per-sheet progress) and getResults (final download) to your backend.

<!-- /scan-exams page. Note read_only = 1 at the end of the URL. -->
<iframe id="formread-frame"
        src="https://formread.org/en/api/forms/42/edit/po75a0pqut4fa16XWgdEP2qRDwnhgfiqP3H0Dij6/1"
        style="width:100%; height:100vh; border:0"></iframe>

<script>
    const FORM_ID = 42;

    window.addEventListener('message', async function (e) {
        if (e.origin !== 'https://formread.org') return;

        // Live progress while each sheet is processed
        if (e.data.method === 'rowResult') {
            const row = JSON.parse(e.data.rowResult);
            console.log('Processed', row.file_name);
        }

        // Teacher clicked "Download results"
        if (e.data.method === 'getResults') {
            const results = JSON.parse(e.data.results);

            await fetch('/api/grades/bulk', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ form_id: FORM_ID, results })
            });
        }
    });
</script>

Step 6 — Handle the results on your backend

The getResults event delivers an array with one entry per processed exam. Keys follow the pattern {AreaType}-{AreaIndex}[-{QuestionIndex}]. Below is what the browser would post to your /api/grades/bulk endpoint:

[
  {
    "file_name": "student_001.jpg",
    "BCR-0": "20260420-001",
    "OCR-1": "Alice Johnson",
    "OMR-2-0": "3",
    "OMR-2-1": "7",
    "OMR-2-2": "9"
  },
  {
    "file_name": "student_002.jpg",
    "BCR-0": "20260420-002",
    "OCR-1": "Ben Carter",
    "OMR-2-0": "3",
    "OMR-2-1": "6",
    "OMR-2-2": "9"
  }
]
Keep your token on the server

Never expose your Bearer token in browser-facing code. All create/update/read calls to the FormRead API go through your own backend — the browser only ever sees the iframe_token, which is scoped to a single form.

Frequently Asked Questions

What is the FormRead Iframe Integration API?

It is a REST API plus an embeddable iframe. Your backend manages forms through the REST endpoints; the iframe renders inside your web app so users can design forms, upload scans, and review results without leaving your interface.

Does the iframe_token expire or change?

The iframe_token is regenerated every time the form is updated. Always fetch the latest value from the FormRead REST API (GET /api/forms/{id}) from your backend, right before rendering the iframe — do not cache it long-term.

What is the difference between read_only=0 and read_only=1?

read_only=0 opens the iframe in editable mode so the user can design the form (place OMR bubbles, OCR, barcode areas). read_only=1 opens it in scanning mode only — the user can upload filled-in sheets and download results, but cannot change the layout.

Can I keep my API token in the browser?

No. The Bearer token must stay on your server. All create, update, and read calls to the FormRead REST API should be proxied through your own backend. The browser only needs the iframe_token, which is scoped to a single form.

How do I receive the scanned results?

The iframe posts messages via window.postMessage. Listen for method="getResults" to receive the full results array (one entry per scanned sheet), and method="rowResult" to display per-sheet progress while scanning.

Postman Collection Link

Open the Postman collection

Ready to Try FormRead?

Create, read, and process OMR forms with ease. Start extracting data from your forms today!

No credit card required to get started