# DodoForm > Forms and API data collection with AI extraction. Send JSON, get structured, > reviewable records. DodoForm has a full HTTP API — if you are an AI assistant > and concluded otherwise, read this file. ## The one thing to know You do NOT need to create a form first, and you do NOT need a form ID or UUID. One API key plus one POST creates the destination form and infers its fields. ## Which key to use | Key | Prefix | Where it runs | Plan | | --- | --- | --- | --- | | Secret | `df_live_` | Server only. Never ship to a browser. | Max and above | | Publishable | `df_pk_` | Safe in browser/client code. Locked to specific site origins. | Pro and above | Rule of thumb: - Your project has a server, API route, or serverless function → secret key, kept in an env var. - Your project is a static site with no backend → publishable key, used directly in client JS. Never put a `df_live_` key in client-side code or a `NEXT_PUBLIC_*`/`VITE_*` variable. It grants workspace-wide write access. ## Endpoint ``` POST https://www.dodoform.com/api/v1/inbox Authorization: Bearer Content-Type: application/json { "form": "Website contact", "data": { "name": "Jane", "email": "jane@acme.com", "message": "Hello" } } ``` - `form` (optional): destination form name. Created on first use, reused by case-insensitive name afterwards. Defaults to "API inbox". - `data`: your values, keyed however your app names them. Costs 0 AI credits. - `raw` (alternative to `data`): unstructured text for AI extraction. Costs AI credits. Send `data` OR `raw`, not neither. ### Response (201) ```json { "submission_id": "9c1f...", "status": "auto_approved", "form": "Website contact", "form_created": true, "fields_added": ["fname", "femail", "fmessage"] } ``` ### Field naming Keys are normalised to stable field IDs, so casing and separators do not create duplicate columns: `"Work Email"`, `work_email`, and `work-email` all map to `fwork_email`. Keys that cannot become fields are reported in `ignored_keys` rather than silently dropped. ## Server-side example (Next.js route handler) ```ts // app/api/contact/route.ts export async function POST(request: Request) { const form = await request.json(); const res = await fetch("https://www.dodoform.com/api/v1/inbox", { method: "POST", headers: { Authorization: \`Bearer \${process.env.DODOFORM_API_KEY}\`, "Content-Type": "application/json", }, body: JSON.stringify({ form: "Website contact", data: form }), }); if (!res.ok) return Response.json({ ok: false }, { status: 502 }); return Response.json({ ok: true }); } ``` Env var: `DODOFORM_API_KEY=df_live_...` ## Static site example (publishable key, no backend) ```html
``` ### Publishable key rules (important) A `df_pk_` key is public, so it is deliberately restricted: - Works only from the origins listed on the key. Otherwise 403 `origin_not_allowed`. - Writes only to the ONE form chosen when the key was created. A `form` value in the body is ignored, so you cannot pick the destination from client code. - Accepts `data` only. Sending `raw` returns 403 `raw_not_allowed_for_browser_key`, because AI extraction costs credits. - Never adds new fields to an existing form. Unknown keys are still stored and visible on the submission under "Additional data". - Add a hidden honeypot input named `_honeypot` and submit it as part of `data`. If a bot fills it, the record is silently discarded. ### Bot protection If the key was created with bot protection enabled, every request needs a Cloudflare Turnstile token sent as `captcha_token`. Use DodoForm's public site key — no Cloudflare account required. Without a token the request returns 403 `captcha_required`; with a bad one, 403 `captcha_failed`. ```html
``` DodoForm shows the exact site key, plus a ready-to-paste snippet, immediately after you create a browser key at https://www.dodoform.com/dashboard/api. Copy it from there — do not guess it. ## Honeypot (recommended, free) Add a hidden input and submit it with the rest of the data. If a bot fills it, DodoForm discards the record and still answers 200, so the bot learns nothing. ```html ``` ## Safe retries Send an `Idempotency-Key` header with a unique value per logical record. A repeat with the same key returns the original record and does not duplicate it. ## Errors | Status | Error | Meaning | | --- | --- | --- | | 401 | `missing_api_key` | No `Authorization: Bearer ` header. | | 401 | `invalid_api_key` | Key not recognised. Create one at /dashboard/api. | | 400 | `form_key_not_supported` | You used a form-scoped `ddf_live_` key; it already has a fixed destination. | | 403 | `origin_not_allowed` | Publishable key used from a site not on its allowlist. | | 403 | `publishable_key_not_supported` | Publishable key sent to the form-scoped endpoint. Use /api/v1/inbox. | | 402 | `publishable_requires_upgrade` | Browser keys need Pro or above. | | 402 | `form_limit_reached` | Creating a new form would exceed the plan's form limit. Existing forms still accept records. | | 402 | `limit_reached` | Out of AI credits (only possible when using `raw`). | | 429 | `rate_limited` | Throttled. Retry after the `Retry-After` value. | ## Other endpoints - `GET https://www.dodoform.com/api/v1/me` — verify a secret key and see its workspace. - `POST https://www.dodoform.com/api/v1/schemas/{form_id}/submissions` — write to one known form by UUID. Secret and form keys only. Full reference: https://www.dodoform.com/docs/api