Headless API for mobile
Don't want an iframe on your mobile app? Use the public API to fetch the form schema and submit responses from your own UI — on Android, iOS, React Native or Flutter. On the web you can use the eesyform-embed package's EesyFormClient for the same calls.
1Fetch the form schema
Send a GET request to the public schema endpoint. You get the published fields, settings, choices and logic rules as JSON — enough to render the form however you like.
GET https://YOUR-EESYFORM-HOST.com/api/public/forms/my-form-abc123HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "form_0f8d",
"slug": "my-form-abc123",
"title": "Customer feedback",
"description": "Tell us about your experience",
"settings": {
"showProgressBar": true,
"welcomeScreen": {
"title": "Hi there!",
"description": "A quick survey, one question at a time.",
"showButton": true,
"buttonLabel": "Start"
},
"thankYouScreen": {
"title": "Thanks for your feedback!",
"description": "Your response has been recorded."
},
"embeddable": true,
"hiddenVariables": []
},
"fields": [
{
"id": "q_9kf2",
"type": "short_text",
"title": "What's your name?",
"description": null,
"required": true,
"properties": { "placeholder": "Ada Lovelace" }
},
{
"id": "q_m8p1",
"type": "multiple_choice",
"title": "How satisfied are you?",
"description": null,
"required": true,
"properties": {
"choices": [
{ "id": "c_1", "label": "Very satisfied" },
{ "id": "c_2", "label": "Somewhat satisfied" },
{ "id": "c_3", "label": "Not satisfied" }
],
"allowOther": true
}
},
{
"id": "q_4t7c",
"type": "number",
"title": "How likely are you to recommend us?",
"description": "0 to 10",
"required": false,
"properties": { "min": 0, "max": 10 }
}
],
"logic": []
}Password-protected forms return 403 and closed forms return 404 so the schema is never leaked.
2Submit responses
Post responses with a Typeform-style payload. Each field carries a field.id and type plus the matching typed value. Required fields must all be present.
POST https://YOUR-EESYFORM-HOST.com/api/public/forms/my-form-abc123/responses
Content-Type: application/json
{
"responses": [
{
"sessionId": "app-session-1234",
"hidden": { "utm_source": "ios-app" },
"fields": [
{
"field": { "id": "q_9kf2", "type": "short_text" },
"text": "Ada Lovelace"
},
{
"field": { "id": "q_m8p1", "type": "multiple_choice" },
"choice": { "label": "Very satisfied" }
},
{
"field": { "id": "q_4t7c", "type": "number" },
"number": 9
}
]
}
]
}HTTP/1.1 201 Created
Content-Type: application/json
{
"responses": [
{ "response_id": "rsp_8d1a", "form_id": "form_0f8d" }
]
}Value shapes by field type
// Value payloads per field type
short_text -> { "text": "Ada" }
long_text -> { "text": "Multiline reply" }
email -> { "text": "ada@example.com" }
phone -> { "text": "+1 555 0100" }
website -> { "text": "https://example.com" }
number -> { "number": 42 }
date -> { "date": "1995-06-15" }
yes_no -> { "choice": { "label": "Yes" } } // or { "boolean": true }
multiple_choice -> { "choice": { "label": "Choice label" } }
dropdown -> { "choice": { "label": "Option label" } }
picture_choice -> { "choice": { "label": "Option label" } }
multiple_select -> { "choices": { "labels": ["A", "C"] } }
ranking -> { "choices": { "labels": ["First", "Second"] } }
rating -> { "number": 5 }
opinion_scale -> { "number": 4 }
legal -> { "boolean": true }
statement -> {} // informational only
file_upload -> { "file_url": "https://..." }
payment -> { "payment": { ... } } // handled in-app by RazorpayErrors
The API validates every request and returns 400 with a descriptive message when something is wrong:
HTTP/1.1 400 Bad Request
{ "error": "Missing required field \"q_9kf2\"" }
HTTP/1.1 400 Bad Request
{ "error": "Unknown field id \"q_wrong\"" }
HTTP/1.1 400 Bad Request
{ "error": "Invalid or missing value for field \"q_4t7c\"" }
HTTP/1.1 403 Forbidden
{ "error": "Form is password protected" }
HTTP/1.1 404 Not Found
{ "error": "Form is closed" }
HTTP/1.1 429 Too Many Requests
{ "error": "Rate limit exceeded" } // 10 req/min per IP
HTTP/1.1 429 Too Many Requests
{ "error": "WORKSPACE_RESPONSE_LIMIT_REACHED" }
HTTP/1.1 429 Too Many Requests
{ "error": "FORM_LIMIT_REACHED" }
HTTP/1.1 402 Payment Required
{ "error": "Payment required before completing this form" }
HTTP/1.1 403 Forbidden
{ "error": "PLAN_REQUIRED", "message": "file_upload requires Pro plan" }file_url and payment values, but the upload/checkout flows themselves run in the hosted form. For those field types, open the embed page in a WebView or browser.3Platform recipes
CORS is open (*), so browsers, native clients and servers can all call the API directly.
Native Android (Kotlin)
Fetch the schema with OkHttp, then render each field in a RecyclerView or Compose list. Required fields are listed in fields[] with their validation constraints under properties.
// Get the form schema
val slug = "my-form-abc123"
val client = OkHttpClient()
val schemaRequest = Request.Builder()
.url("https://YOUR-EESYFORM-HOST.com/api/public/forms/$slug")
.build()
client.newCall(schemaRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("EesyForm", "Failed to load schema", e)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
Log.e("EesyForm", "HTTP ${response.code}: ${response.body?.string()}")
return
}
val schema = JSONObject(response.body?.string() ?: "{}")
val fields = schema.getJSONArray("fields")
// Render each field in your UI...
}
})// Submit a response
val body = JSONObject().apply {
val responses = JSONArray()
val r = JSONObject().apply {
put("sessionId", "android-1234")
put("hidden", JSONObject().put("utm_source", "android"))
val fields = JSONArray()
fields.put(JSONObject().apply {
put("field", JSONObject().apply {
put("id", "q_9kf2")
put("type", "short_text")
})
put("text", "Ada Lovelace")
})
fields.put(JSONObject().apply {
put("field", JSONObject().apply {
put("id", "q_m8p1")
put("type", "multiple_choice")
})
put("choice", JSONObject().put("label", "Very satisfied"))
})
put("fields", fields)
}
responses.put(r)
put("responses", responses)
}
val submitRequest = Request.Builder()
.url("https://YOUR-EESYFORM-HOST.com/api/public/forms/my-form-abc123/responses")
.post(body.toString().toRequestBody("application/json".toMediaType()))
.build()
client.newCall(submitRequest).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (response.code == 201) {
Log.i("EesyForm", "Response submitted")
} else {
Log.e("EesyForm", "HTTP ${response.code}: ${response.body?.string()}")
}
}
override fun onFailure(call: Call, e: IOException) {
Log.e("EesyForm", "Submission failed", e)
}
})Native iOS (Swift)
import Foundation
struct FormSchema: Decodable {
let id: String
let slug: String
let title: String
let fields: [FormField]
}
struct FormField: Decodable, Identifiable {
let id: String
let type: String
let title: String
let required: Bool
let properties: [String: JSONValue]?
}
func fetchSchema(slug: String) async throws -> FormSchema {
let url = URL(string: "https://YOUR-EESYFORM-HOST.com/api/public/forms/\(slug)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(FormSchema.self, from: data)
}struct SubmitBody: Encodable {
struct FieldValue: Encodable {
struct FieldRef: Encodable {
let id: String
let type: String
}
let field: FieldRef
let text: String?
let number: Int?
let choice: ChoiceRef?
}
struct ChoiceRef: Encodable {
let label: String
}
let responses: [Response]
struct Response: Encodable {
let sessionId: String?
let fields: [FieldValue]
}
}
func submitResponses(fields: [SubmitBody.FieldValue]) async throws {
var request = URLRequest(
url: URL(string: "https://YOUR-EESYFORM-HOST.com/api/public/forms/my-form-abc123/responses")!
)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = SubmitBody(
responses: [
.init(sessionId: "ios-1234", fields: fields)
]
)
request.httpBody = try JSONEncoder().encode(body)
let (_, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 201 else {
throw URLError(.badServerResponse)
}
}React Native
React Native can use the package's typed client directly — no fetch boilerplate or hand-rolled types.
import { useEffect, useState } from "react";
import {
ActivityIndicator,
FlatList,
Pressable,
Text,
TextInput,
View,
} from "react-native";
import { EesyFormClient, type FormSchema } from "eesyform-embed/client";
const client = new EesyFormClient("https://YOUR-EESYFORM-HOST.com");
export function RemoteForm({ slug }: { slug: string }) {
const [schema, setSchema] = useState<FormSchema | null>(null);
const [error, setError] = useState<string | null>(null);
const [answers, setAnswers] = useState<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
client
.getSchema(slug)
.then(setSchema)
.catch((e: Error) => setError(e.message));
}, [slug]);
if (error) return <Text style={{ color: "red" }}>{error}</Text>;
if (!schema) return <ActivityIndicator />;
async function submit() {
setSubmitting(true);
try {
await client.submitResponses(slug, {
responses: [
{
sessionId: "rn-1234",
fields: schema.fields
.filter((f) => answers[f.id] !== undefined)
.map((f) => ({
field: { id: f.id, type: f.type },
...(answers[f.id] as object),
})),
},
],
});
Alert.alert("Done", "Response recorded");
} catch (e) {
Alert.alert("Error", (e as Error).message);
} finally {
setSubmitting(false);
}
}
return (
<FlatList
data={schema.fields}
renderItem={({ item }) => (
<TextInput
placeholder={item.title}
onChangeText={(value) =>
setAnswers((prev) => ({
...prev,
[item.id]: { text: value },
}))
}
/>
)}
ListFooterComponent={
<Pressable disabled={submitting} onPress={submit}>
<Text>{submitting ? "Submitting..." : "Submit"}</Text>
</Pressable>
}
/>
);
}Flutter
Uses the http package. Pair with json_serializable models for type safety.
import 'dart:convert';
import 'package:http/http.dart' as http;
const host = "https://YOUR-EESYFORM-HOST.com";
Future<Map<String, dynamic>> fetchSchema(String slug) async {
final res = await http.get(
Uri.parse('$host/api/public/forms/$slug'),
);
if (res.statusCode != 200) {
throw Exception('Failed to load form: ${res.body}');
}
return jsonDecode(res.body) as Map<String, dynamic>;
}
Future<void> submitResponse(List<Map<String, dynamic>> fields) async {
final payload = jsonEncode({
'responses': [
{
'sessionId': 'flutter-1234',
'hidden': {'utm_source': 'flutter'},
'fields': fields,
},
],
});
final res = await http.post(
Uri.parse('$host/api/public/forms/my-form-abc123/responses'),
headers: {'Content-Type': 'application/json'},
body: payload,
);
if (res.statusCode != 201) {
throw Exception('Submission failed: ${res.body}');
}
}
// Usage:
// final schema = await fetchSchema('my-form-abc123');
// final fields = (schema['fields'] as List).cast<Map<String, dynamic>>();
// await submitResponse([
// {
// 'field': {'id': 'q_9kf2', 'type': 'short_text'},
// 'text': 'Ada Lovelace',
// },
// ]);
void main() async {
final schema = await fetchSchema('my-form-abc123');
print('Form: ${schema['title']}');
await submitResponse([
{
'field': {'id': 'q_9kf2', 'type': 'short_text'},
'text': 'Ada Lovelace',
},
]);
}