Developers
A clean contract, by design.
One canonical JSON schema underpins the editor, validation and the AI. What you build against won't drift out from under you.

The canonical schema is the contract
A form is a single typed schema, defined once and compiled to JSON Schema. The form editor, server-side validation and the AI authoring all read from that same definition - there is no second model to fall out of sync. Practically, that means the shape of an exported response is stable and predictable, and an AI-generated draft is just data in the same schema, not an opaque blob you have to accept on faith.
Answers always reference stable question_id and option_id values, never display labels. You can rename a question or reword an option without breaking historical responses or any downstream pipeline keyed on those IDs.
Webhooks
Point a form at any HTTPS endpoint and Askery POSTs a single JSON payload on each submission. One event, response.submitted, fires once per response - no ordering games. For plain forms it fires immediately; for forms with Form Intelligence on we wait for the AI result and include an intelligence object in the same delivery, so your endpoint always handles one webhook per submission regardless of form configuration. Deliveries retry with backoff on failure and every attempt is recorded in a delivery log you can inspect.
Payload shape
{
"event": "response.submitted",
"created_at": "2026-05-19T10:00:00Z",
"form": { "id": "f3a7…", "title": "Pricing feedback" },
"response": {
"id": "r9b2…",
"answers": [
{ "question_id": "email", "value": "ada@example.com" },
{ "question_id": "rating", "value": 9 },
{ "question_id": "topics", "value": ["billing", "api"] }
]
}
}When the form has Form Intelligence on, the same delivery carries an extra intelligence object with the AI result (engine, sections, optional score / archetype / links):
{
"event": "response.submitted",
"created_at": "2026-05-19T10:00:00Z",
"form": { "id": "f3a7…", "title": "Best universities for you" },
"response": { "id": "r9b2…", "answers": [/* … */] },
"intelligence": {
"model": "decision-engine",
"generatedAt": "2026-05-19T10:00:02Z",
"engine": "decision_engine",
"richSections": [
{ "type": "list", "id": "countries", "title": "Best countries", "items": ["Germany","Netherlands"] },
{ "type": "label", "id": "verdict", "label": "Strong research fit" }
],
"score": 78,
"archetype": "The Practical Innovator"
}
}Signed-in customers can toggle every payload variant live in our interactive explorer at /dashboard/developers.
Verifying the signature
Each request carries an X-Askery-Signature header in the form sha256=<hex> - the HMAC-SHA256 of the raw request body, keyed with the per-endpoint signing secret shown when you create the webhook. Compute the same HMAC over the raw bytes you received (before any JSON parsing), strip the sha256=prefix from the header, and compare with a constant-time check. Reject the request if it doesn't match.
import crypto from "node:crypto";
function verify(rawBody: string, header: string, secret: string) {
// Header format: "sha256=<hex>" - strip the prefix.
const provided = header.startsWith("sha256=")
? header.slice("sha256=".length)
: header;
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
return (
provided.length === expected.length &&
crypto.timingSafeEqual(
Buffer.from(provided, "utf8"),
Buffer.from(expected, "utf8"),
)
);
}Treat delivery as at-least-once: dedupe on response_idso a retried delivery doesn't create duplicate downstream records.
Export formats
Responses export as CSV for spreadsheets and BI tools, or as JSON when you want structure preserved. Both are lossless and derived from the canonical schema, so columns and keys stay coherent across form edits and versions. There is no proprietary export format to reverse-engineer - see the integrations overview.
Embedding a form
Three ways to drop a form onto your site, depending on what your host page supports. The first one is what we recommend for most contact / support / lead-capture surfaces: a 1-line script that inherits your page's fonts and colors automatically and resizes the iframe to fit the form's content - no scrollbars, no dead space.
Modern embed (recommended)
Script once in <head>, drop the placeholder where the form should appear. Inherits host fonts and colors, auto-resizes. The preconnect line is what makes it feel instant - without it the browser delays DNS until the iframe is created.
<!-- in <head> --> <link rel="preconnect" href="https://askery.app" crossorigin> <script async src="https://askery.app/embed.js"></script> <!-- where the form should appear --> <div data-askery-form="your-form-slug"></div>
Pass data-askery-theme="form"on the placeholder if you want the form's own theme to win over the host page's styles. Pre-fill any field with data-askery-prefill-FIELD_ID="value" (great for hidden UTM fields). Add data-askery-loading="lazy" to defer load until the form scrolls into view.
Classic iframe (no JavaScript, fastest first paint)
Pure HTML, no script. The browser starts fetching the form in parallel with the host page, so first paint is faster than the script-driven embed. Trade-offs: uses the form's own theme (no host inheritance) and a fixed height.
<!-- in <head>, optional but faster --> <link rel="preconnect" href="https://askery.app" crossorigin> <!-- where the form should appear --> <iframe src="https://askery.app/f/your-form-slug" title="Contact form" loading="eager" style="width:100%;border:0;min-height:640px"> </iframe>
Popup button
Render a button anywhere - header, footer, sticky widget. The form opens in a centered modal when clicked. Same auto-resize + theme bridge as the modern embed.
<!-- in <head> --> <link rel="preconnect" href="https://askery.app" crossorigin> <script async src="https://askery.app/embed.js"></script> <!-- anywhere in the page --> <button data-askery-form-popup="your-form-slug" type="button">Talk to us</button>
Prefill links
Seed any field - including hidden tracking fields - via query parameters, so campaign attribution arrives with the response:
https://askery.app/f/your-form?email=ada@example.com&ref=docs
Public API - full read + write
A documented, scope-gated REST API. Bearer-token authentication, one canonical JSON contract, version-prefixed at /api/v1/*. Every checkbox in the builder is a JSON field; every AI capability is an endpoint; the canonical schema the UI saves is the same shape the API accepts.
What you can do:
- Forms - list, create, get, JSON-merge-patch, full replace, duplicate, publish, archive, restore, delete; audit-grade version history with one-click rollback.
- AI - generate a form from a natural-language brief, AI-edit a stored form with chat history, apply deterministic ops, and codegen a Decision Engine from a brief.
- Responses - list with rich filters (status, email, since/until), cursor pagination, optional folded answers, optional cached AI outcome; export CSV or JSONL; delete; regenerate an AI outcome on demand.
- Form Intelligence + Decision Engine - read config, save validated code, test code in the WASM sandbox against synthetic answers (0 credits).
- Import - point at any form URL (Google Forms parsed deterministically, others via LLM) and receive a canonical FormDefinition.
- Webhooks - full CRUD on subscriptions plus a recent delivery log per webhook.
- Workspace - workspace identity, plan, credit balance, audit log, per-form analytics; build internal admin dashboards without scraping the UI.
Full OpenAPI 3.1 spec at /api/v1/openapi.json (importable into Postman / Insomnia / openapi-typescript). Interactive reference, key minting and audit log live in the dashboard at Developers.
Scopes
Keys are scope-gated - narrow keys never see more than they should:
forms:read,forms:writeresponses:read,responses:writeai:run- invoke generate / edit / decision-codeintelligence:run- Decision Engine sandbox + regeneratewebhooks:write,imports:runworkspace:read- identity, plan, credits, audit log
Idempotency + errors
Every mutation accepts an Idempotency-Key header (≤256 chars). Re-sending the same key replays the stored response within 24h; a different body with the same key returns 409 idempotency_conflict. Error envelope:
{ "error": {
"code": "form_not_found",
"message": "No form with id 'abc'.",
"request_id": "req_abc123…",
"docs_url": "https://askery.app/docs/api/errors/form_not_found"
}
}MCP - operate Askery from any AI agent
The Askery MCP server is a Model Context Protocol bridge over the public API. Drop it into Claude Desktop, Cursor, or any MCP-capable agent and it can build forms, fetch responses, generate Decision Engine code, and manage webhooks - by name, out of the box.
{
"mcpServers": {
"askery": {
"command": "bunx",
"args": ["--bun", "/path/to/askery/mcp/server.ts"],
"env": { "ASKERY_API_KEY": "ak_live_..." }
}
}
}Full tool catalog and setup at /mcp.
Security model
Public submissions never touch raw table inserts - they flow through a single audited, security-definer write path, and the server re-validates every submission against the canonical schema before it is stored. Tenant isolation is enforced in Postgres with row-level security rather than application code. The full model is on the security page.
Questions or building something specific? Email support@askery.app or use the contact form.