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.
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.
- Authenticate with a Bearer token (generated from the dashboard).
- Use the
POST /api/formsendpoint to create a form and get theiframe_token. - Embed the iframe in your page using the
iframe_tokento let users edit forms and scan documents. - 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:
Create the API tokens to allow third-party services to authenticate with our application on your behalf:
Copy the token generated in a secure place (it will only be shown once):
Create New Form
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
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:
formDatavariable will contain the encoded form attributes that can be sent using the Updateto save the changes made to your form:-
the
schemavariable 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
schemavariable 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
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
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.
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"
}
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"
}
]
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
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