Forms developer docs
The Forms developer API
A read-only OAuth 2.0 interface to the forms one business owns, the revisions of those forms and the responses they collected. This page is the overview. The machine-readable contract is served from the API itself.
Base URL and scope
https://api.firstaiemployee.com/api/developer/v1/formsEvery path on this page is relative to that URL. One scope reads all of it: forms:read. There is no write half, so there is no second scope to ask for.
The business is selected by the access token and by nothing else. No path, query or body parameter can name a business, and an unknown query parameter is refused with 400 rather than ignored.
The surface is behind two switches on our side, and both must be on. When it is off, every path answers 404, so a caller with no token cannot learn that a business exists or that the resource is there.
Getting a token
Authentication is the same OAuth 2.0 authorization code flow the rest of our Client API uses: GET /oauth/authorize to send the customer to consent, then POST /oauth/token to exchange the one time code. PKCE with S256 is required, and the redirect URI must match a registered one exactly.
- Registration is manual. There is no self service endpoint that hands out client credentials.
- A registration that asks for forms:read must carry the response data acknowledgement. The registration tool refuses the scope without it.
- The customer approves the scope for one business, and can disconnect the grant afterwards from Connected Apps.
- Send the access token as a Bearer credential. A customer session cookie never authorizes this API.
Why that acknowledgement exists
A form response is whatever the business asked its own customers to type: names, addresses, phone numbers and free text nobody screened. That is a wider class of data than a call summary or a booking window, so granting it is a decision somebody states out loud rather than a scope that arrives with a copied command line. Plan for it before you ask for the scope: minimize what you copy, encrypt it where you put it, and give it a retention period you can name.
curl "https://api.firstaiemployee.com/api/developer/v1/forms?limit=25" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Accept: application/json"Endpoints
| Path | Returns | Default limit |
|---|---|---|
| GET / | One page of the forms this business owns, most recently edited first. | 25 |
| GET /{id} | One form, its published definition, and the revision that definition came from. Null definition until the form is published. | not a list |
| GET /{id}/revisions | One page of published revisions, newest first. | 25 |
| GET /{id}/revisions/{number} | The frozen definition for one published version number, with its fingerprint. | not a list |
| GET /{id}/responses | One page of responses, newest first, with the filter that produced it. | 50 |
| GET /openapi.json | The OpenAPI 3.1 document for this surface. It is behind the same token as the data. | not a list |
Every list takes ?limit and ?cursor. A limit is a whole number from 1 to 100, and a limit outside that range is a 400 rather than a silently clamped page.
Filtering responses
| Parameter | Accepts |
|---|---|
| status | Comma separated: partial, completed, spam. The default is partial and completed, so responses caught as spam stay out unless you ask for them. |
| from | Inclusive start. A YYYY-MM-DD day is read in the business time zone. A full ISO timestamp is read as written. |
| to | Exclusive end, read the same way as from. A day ends at the following midnight in the business time zone. |
| revision | A published version number, or any. Absent means any revision. |
A response page carries the filter that produced it: filterFingerprint is a stable hash and filterDescription is one sentence naming the population. Two counts with the same fingerprint describe the same population. Two with different fingerprints do not.
Every response names the revision it answered. revisionId and revisionFingerprint are null only when the definition history for that version is gone, which happens for versions published before this product stored revisions. Filter by revision to read only the responses that answered one published version. A revision is append-only: the definition on a revision that already has responses never changes.
What a definition carries
A published definition carries the same content a respondent browser receives, plus the branching logic and a redacted mode block. The mode block says what the form works out and what its outcomes are called. It never carries the answer key, the score a band starts at, or the condition a band or a qualification test uses.
Two response columns are deliberately absent from version 1. The respondent browser metadata is out because an integration does not need it. The variable bag is out because on a partial response it holds a live running score, and a partner who could read that score while also posting answers could recover the marking scheme one option at a time.
Paging
{
"data": [ /* forms */ ],
"meta": {
"limit": 25,
"hasMore": true,
"nextCursor": "opaque_cursor_returned_by_this_api"
}
}Send meta.nextCursor back unchanged. A cursor is opaque, it carries no business identity, and one this API did not issue is refused rather than interpreted.
Pages are keyset pages, newest first. A form list is ordered by the last edit time, then by id. A response list is ordered by the submission time, falling back to the time the respondent landed, then by id. A cursor is a position in that order, not a snapshot: rows that change while you page can move. A partial response that completes while you page gets a new submission time, so it can appear twice or be skipped. Deduplicate by id, and reconcile from the first page after a long read.
Revisions page differently
Revisions page on the published version number, highest first. A revision is append-only, so a row already on a page can never change, move or disappear: the only thing a later publish does is add a higher number at the front. A cursor taken now stays valid, and paging to the end reads each revision exactly once.
Answers and hidden values are objects
The answers, hidden and calculated blocks are JSON objects, and JSON objects have no order. They are stored as jsonb, so a later read can hand the same keys back in a different order from the one you saw first. Match on the names. The same rule holds for a webhook delivery.
Rate limits
The published budget is 120 requests a minute for each pair of application and business, keyed from the verified grant. A partner behind one address is not one quota, and a shared network address does not spend somebody else's budget.
In front of that there is a second stage, spent per address before the token is looked up, so an unauthenticated flood cannot buy one database read per request. It ships at 1,200 requests a minute, well above the published budget, and an honest integration does not meet it.
Both stages answer 429 with the same body and a Retry-After header. Honor it and back off. Do not key retry logic off the message text.
The error envelope
Every failure has the same shape: one error object with a stable code, a message for a human, and the request id to quote at us. A validation failure adds an errors array that names each parameter.
{
"error": {
"code": "rate_limited",
"message": "Too many Forms API requests. Wait for the period named by Retry-After.",
"requestId": "req_01J..."
}
}{
"error": {
"code": "invalid_request",
"message": "Some of those query parameters do not work here.",
"requestId": "req_01J...",
"errors": [
{
"path": "customerId",
"message": "is not a parameter this endpoint accepts. Allowed: cursor, limit."
}
]
}
}| Status | Meaning | What to do |
|---|---|---|
| 400 | The query names a parameter this endpoint does not accept, or a value it cannot read. | Fix the request. An unknown parameter is refused rather than ignored, on purpose. |
| 401 | Missing, invalid, expired or revoked access token. | Refresh once, then reconnect. The response carries WWW-Authenticate. |
| 403 | The token lacks the forms:read scope, or Forms is turned off for this business. | Ask the customer to authorize the scope. Do not retry unchanged. |
| 404 | No form with that id belongs to this business. | Treat as absent. A form owned by another business answers 404 as well, never 403. |
| 429 | More than the per token budget, or more than the per address ingress budget. | Honor Retry-After. The body carries code rate_limited. |
| 500 | Something failed on our side. The body names no internal detail. | Retry with backoff. The request was not a write, so a retry costs nothing. |
| 503 | The entitlement state for this business could not be read, so the request is refused rather than answered from a guess. | Retry with backoff. |
There is no submit endpoint here
There is no developer-token endpoint for submitting a response, and this is deliberate. First AI Employee already publishes a server-validated public submit endpoint that a respondent browser or your own server can call with no owner credential: the form id is the capability, the server re-walks the published definition, revalidates every answer, recomputes variables and picks the ending. Submitting through that endpoint keeps your integration free of a tenant credential that could forge answers. Read the hosted form submission contract for the request shape, the abuse limits and the version semantics.
The OpenAPI document
The machine-readable contract is generated from the same constants and the same examples the handlers use, so a documented example is one a handler really produces. It sits behind the same token as the data, which means an unauthenticated caller learns nothing from it, not even that the surface is there.
curl "https://api.firstaiemployee.com/api/developer/v1/forms/openapi.json" \
-H "Authorization: Bearer ACCESS_TOKEN"Related
To put a form on your own site, see the embed contract. To have each response pushed to you as it arrives instead of polling for it, see the setup guides for Zapier, Make and n8n.