formulize-public-api/v1/form/{form}/read

Returns entries from a form as JSON. {form} can be the form handle or the form id.

Send a POST with a JSON body, or a GET with query string parameters. POST is recommended: it carries nested filters comfortably, and it keeps an API key out of server logs and browser history.

If the API is not enabled, a 503 http error is returned.

Authentication

A request with an API key, sent as an Authorization: Bearer header, runs as the user the key belongs to and sees exactly what that user can see. A request without one runs as the anonymous user, and only sees forms that have been opened to the Anonymous group.

See Authentication on the Public API page for how keys work, and how to keep them safe.

Parameters

Parameter Description
fields Required. The element handles to return. Metadata field names such as creation_datetime can be included too. entry_id is always returned.
filter Which entries to return. See Filters below, and the Filters page for everything filters can do.
andOr AND or OR, between the top level items of filter. Defaults to AND.
sortField An element handle or metadata field name to sort by. Defaults to entry_id.
sortOrder ASC or DESC. Defaults to ASC.
limitStart The first record to return, counting from 0. Defaults to 0.
limitSize How many records to return. Defaults to 100, and cannot exceed 10000. Use null for no limit.
relationship 0, the default, returns data from this form only. -1 uses the Primary Relationship, so data from connected forms is included as well.
raw true returns raw database values instead of readable ones. A list of element handles returns just those fields raw.

Requesting a field you do not have permission to see is an error, not a silent omission. fields is required because it decides how much data has to be gathered and converted, so asking for only what you need keeps the request fast.

Filters

filter decides which entries are returned. It is a list of conditions, each with an element and a value, and optionally an operator. The operator defaults to LIKE, which is a partial text match:

"filter": [
  {"element": "donor_type", "value": "major"},
  {"element": "amount", "value": "100", "operator": ">"}
]

The conditions are joined by andOr, which is AND unless you say otherwise, so this returns major donors who gave more than 100.

Filters can do a lot more: mix AND and OR, find blank values, and find entries that have no connected entry matching a condition. The Filters page covers all of it.

The response

{
  "data": [
    { "entry_id": 412,
      "donor_name": "Aiko Tanaka",
      "amount": "250.00" }
  ],
  "meta": { "form": "donors", "count": 1, "limitStart": 0, "limitSize": 100 }
}

Page through a large result by increasing limitStart by limitSize until fewer rows come back than you asked for.

When relationship is set, entries from connected forms appear under related inside the row they belong to, grouped by form handle. Each one carries its own entry_id, from its own form:

{ "entry_id": 412,
  "country_name": "Canada",
  "related": {
    "cities": [
      {"entry_id": 88, "city_name": "Toronto"},
      {"entry_id": 91, "city_name": "Halifax"}
    ]
  } }

related is left out entirely when there is nothing connected.

Errors

Errors return an http status code and a body like this:

{ "error": { "code": "permission_denied",
             "message": "You do not have permission to view this form",
             "hint": "This request carried no Authorization header, so it was handled as anonymous..." } }

message is a plain explanation that is suitable to show to people. code does not change, so use it when your code needs to react to a particular error. hint is only included on some errors, and is aimed at the developer: it explains the likely cause, such as a missing API key.

Two errors have no body at all, only the status code: 503 when the Public API is not enabled, and 404 when the address does not match any part of the API, such as a misspelling of form in the URL. Read the body with that in mind.

Status When
400 A parameter is missing or not valid, including a field name that does not exist
401 An API key was supplied but is not valid or has expired
403 This user, or the anonymous user, may not view this form
404 No such form, or no such method
405 An http method other than GET or POST
503 The Public API is not enabled

Examples

Calling from another website

Javascript on another website can only read the response if an administrator has listed that site under Websites allowed to call the Public API, in Settings → Advanced → Public API. Leave that setting blank and no other website can call the API from a browser.

That setting controls web browsers. It is not a substitute for permissions: what any caller can read is still decided by Formulize permissions.

First, a small function that makes the request and turns every kind of failure into an error with a useful message. The rest of the examples use it:

async function readForm(form, body) {
  let res;
  try {
    res = await fetch(`https://example.org/formulize-public-api/v1/form/${form}/read`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });
  } catch (networkError) {
    // fetch only fails like this when no response could be read at all: the site is
    // unreachable, or it has not allowed this website to call the Public API.
    throw new Error('Could not reach the Public API. The site may be down, or may not allow requests from this website.');
  }

  // Most errors carry a JSON body, but a 503 or an unknown address has none, so don't count on one.
  const json = await res.json().catch(() => null);

  if (!res.ok) {
    const error = new Error(json?.error?.message ?? `The Public API responded with http status ${res.status}`);
    error.status = res.status;
    error.code = json?.error?.code;
    error.hint = json?.error?.hint;
    throw error;
  }
  return json;
}

And a small function for putting data on the page. It makes an element and adds whatever you pass it as the contents. Text is always added as text, never as HTML, so values from the API cannot run as code. See Cross-site Scripting Risks for why that matters.

function el(tag, ...contents) {
  const element = document.createElement(tag);
  element.append(...contents);
  return element;
}

Reading data into a web page

This example reads from a form that has been opened to the Anonymous group. First, the HTML where the data will go:

<p>Number of donors: <span id="count"></span></p>
<ul id="list"><li>Loading donors...</li></ul>

The loading message is part of the page, so it shows as soon as the page does, and no code is needed to take it away. When the data arrives, replaceChildren() puts it in place of the message, and if something goes wrong, the error message replaces it instead. Then the Javascript:

async function loadDonors() {
  const { data, meta } = await readForm('donors', {
    fields: ['donor_name', 'amount'],
    filter: [ { element: 'amount', value: '100', operator: '>' } ],
    sortField: 'amount',
    sortOrder: 'DESC',
    limitSize: 50
  });

  // The data is in hand, so everything from here is ordinary synchronous code.
  document.querySelector('#count').textContent = meta.count;
  // Make an <li> for each entry, then put them all in the list in place of what was there.
  // el() and replaceChildren() both add values as text, never as HTML.
  document.querySelector('#list').replaceChildren(
    ...data.map(d => el('li', `${d.donor_name} - ${d.amount}`))
  );
}

loadDonors().catch(err => {
  // Show the reason on the page. textContent displays it as plain text, never as HTML.
  document.querySelector('#list').textContent = `Could not load donors: ${err.message}`;
  // The hint is meant for you, not your visitors, so it goes to the console.
  console.error(err.message, err.code ?? '', err.hint ?? '');
});

An async function returns a promise, so always attach a catch where you call it, or errors will pass silently. Showing err.message matters: a page that only says Could not load donors hides whether the problem is permissions, a mistyped field name, or the API being turned off.

To react to a particular error, check err.code or err.status rather than the wording of the message:

loadDonors().catch(err => {
  const list = document.querySelector('#list');
  if (err.code === 'permission_denied') {
    list.textContent = 'This list is not public yet.';
  } else {
    list.textContent = `Could not load donors: ${err.message}`;
  }
  console.error(err.message, err.code ?? '', err.hint ?? '');
});

The same request with an API key, which runs as the key’s user instead of the anonymous user. Anyone who loads the page can read the key, so see Authentication before using one this way. Add the key to the headers in readForm:

const API_KEY = '8f3ca19d...';

// in readForm:
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },

Reading two forms at once, by starting both requests before waiting for either. If either request fails, Promise.all fails with that request’s error, so the same catch handles it:

const [donors, events] = await Promise.all([
  readForm('donors', { fields: ['donor_name', 'amount'] }),
  readForm('events', { fields: ['event_name', 'event_date'] })
]);

Rendering connected forms:

const { data } = await readForm('countries', {
  fields: ['country_name', 'city_name'],
  relationship: -1
});

document.querySelector('#out').replaceChildren(...data.flatMap(country => [
  el('h2', country.country_name),
  el('ul', ...(country.related?.cities ?? []).map(city => el('li', city.city_name)))
]));

Cross-site Scripting Risks

Do not put values from the API into innerHTML, outerHTML, insertAdjacentHTML() or other functions and methods that treat a string as HTML. The Formulize API returns values exactly as they were entered, regardless of whether you ask for raw values (in the read method, raw values as a concept relates to things like foreign keys in the Formulize database).

When Formulize displays data in its own screens it makes the data safe to show, but when you request data through the API, you get exactly what is in the database. It’s your job to take appropriate steps to make it safe.

A value like <img src=x onerror="..."> is returned as those characters, and a rich text field is returned as its HTML. Put that into innerHTML and the browser runs it as part of your page. Anyone who can fill in the form in Formulize, including anonymous visitors if the form allows them, could then run their own script on your website, for everyone who visits it.

This is called cross-site scripting (XSS). To show values safely, build the elements yourself and add values to them as text. textContent, append() and replaceChildren() all treat a string as plain text, which is what the examples above use. innerHTML, outerHTML and insertAdjacentHTML() treat a string as HTML, so never give them values from the API. If you really need to show a rich text field’s formatting, clean the HTML first with a sanitizer such as DOMPurify: DOMPurify.sanitize(value).

The greatest risk is when a form in Formulize is open for public submissions, and you are displaying data from those submissions. If there are restrictions on who can submit data, the risk is lower, but the risk is never zero, because even trusted users can have their accounts stolen, hijacked, etc, and then data that you might believe is trusted can actually be malicious.

From a server, a script, or a tool such as Zapier or Make

curl -X POST https://example.org/formulize-public-api/v1/form/donors/read \
  -H "Authorization: Bearer 8f3ca19d..." \
  -H "Content-Type: application/json" \
  -d '{"fields":["donor_name","amount"],"limitSize":25}'

The same request as a GET, for quick testing. Lists are comma separated:

curl "https://example.org/formulize-public-api/v1/form/donors/read?fields=donor_name,amount&limitSize=25" \
  -H "Authorization: Bearer 8f3ca19d..."