Skip to content

DodoForm API Reference

v1

Start here

Connecting your own website to DodoForm takes one credential and one request. No form to create first, no UUID to look up, no fields to define. Grab an API key from /dashboard/api and POST your form data as-is:

The only call you need
curl -X POST https://www.dodoform.com/api/v1/inbox \
  -H "Authorization: Bearer df_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "form": "Website contact",
    "data": {
      "name": "Jane Doe",
      "email": "jane@acme.com",
      "message": "I would like a demo."
    }
  }'

Fields are detected automatically

The first call creates a form called "Website contact" and turns your JSON keys into fields — email becomes an email field, message a long-text field, and so on. Later calls reuse that form, and any new key you send becomes a new column.

Structured data costs 0 AI credits — detection is rule-based, not AI.

That's the whole integration. Everything below is for when you want more control — sending to a specific existing form, extracting fields from unstructured text, or handing a narrowly-scoped credential to a third party.

Authentication — two key types

DodoForm has two kinds of API key. Either one can send records — pick whichever suits you and move on. The difference is only how much they can reach.

KeyLooks likeCreated inWorks on
Workspace keydf_live_…/dashboard/apiEvery endpoint — sending records to any form in the workspace, plus /api/v1/me and /api/parse. Simplest choice.
Form keyddf_live_…/dashboard/forms/{id}/apiSending records to one form only. Use when handing a credential to a third party.
Browser keydf_pk_…/dashboard/api/api/v1/inbox only, from the site origins you list. Safe to ship in client-side code — for static sites with no backend. Pro and above.

Not sure which to use?

If your code runs on a server — an API route, serverless function, or backend — use a workspace key from /dashboard/api. It works on every endpoint, including record ingestion for any form you own.

If your site is static with no backend, a secret key would be exposed in the page source. Use a browser key instead. Reach for a form key only when you want a server credential that can touch exactly one form.

Header (identical for every key type)
Authorization: Bearer df_live_your_workspace_key_here

Keys are shown once. Copy the secret at creation — it is stored only as a SHA-256 hash and cannot be retrieved again. Revoke and recreate if lost.

Use the www host. www.dodoform.com serves the API; the bare apex domain does not.

Plan requirement. Creating API keys of either type requires the Max plan or above. See pricing. If you don't want to upgrade, use the no-key option below.

Endpoints

This is the complete v1 surface. Anything not listed here does not exist — in particular there is no /api/v1/submissions and no /api/v1/forms; both return 404.

Finds or creates a form by name, infers fields from your payload, then stores the record. Accepts a workspace key (df_live_…) from your server, or a browser key (df_pk_…) from client-side code. A form-scoped key already has a fixed destination, so it isn't accepted here.

Body

NameTypeRequiredDescription
formstringOptionalDestination form name, matched case-insensitively. Created on first use. Defaults to "API inbox".
dataobjectOptionalYour values, keyed however your app names them. Keys become fields. 0 AI credits.
rawstringOptionalUnstructured text for AI extraction into the form's existing fields. Costs AI credits.

Send data or raw — at least one is required.

201 Created — first call
{
  "submission_id": "9c1f...",
  "status": "auto_approved",
  "structured_data": { "fname": "Jane Doe", "femail": "jane@acme.com" },
  "min_confidence": 1,
  "form": "Website contact",
  "form_id": "a1b2c3d4-...",
  "form_created": true,
  "fields_added": ["fname", "femail", "fmessage"],
  "max_fields_per_form": 60
}

How keys become fields

Stable. "Work Email", work_email and work-emailall map to the same column, so changing casing won't duplicate it.

Typed.The value decides when it's clear (a real address → email; true → toggle; 2026-07-28 → date), otherwise the key name hints.

Guarded. Keys must start with a letter and stay under 40 characters. Reserved names (id, __proto__) are refused, and a form accepts at most 60 auto-created fields.

Transparent. Anything skipped comes back in ignored_keys with a reason — nothing is dropped silently.

Auto-created forms use a plan slot

A form created here counts toward your plan's form limit, exactly like one built by hand. Send many different form names and you will hit that limit and get 402 form_limit_reached (the response lists your existing form names). Reuse one name per integration.

Provisioning is rate limited to 30 requests/hour per workspace. High-volume traffic should post to the form-scoped endpoint below, which allows far more.

No API key? Two options that work on any plan

API keys need the Max plan. If you just want your own site's form to file into DodoForm, you don't need one.

1. Embed the form (zero code)

Publish the form and drop its embed snippet into your page. Responses land in DodoForm with no integration work at all. After publishing you get a ready-made <iframe> to copy, alongside the public link and QR code.

2. Post to the public endpoint (keep your own UI)

Your custom form posts to the same endpoint the public form uses. No key, and sending pre-structured values costs 0 AI credits. Use the form's public slug, not its UUID.

Public submission
curl -X POST https://www.dodoform.com/api/process-submission \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "your-form-slug",
    "structured_data": {
      "name": "Jane Doe",
      "email": "jane@acme.com",
      "message": "I was charged twice this month."
    }
  }'

Bot protection applies

This endpoint is public, so it is rate limited per IP and per form. If the form owner has Cloudflare Turnstile enabled, a valid turnstile_token is required and server-to-server calls will be rejected — use a form key and the /api/v1/… endpoint for backend-to-backend traffic.

What costs AI credits

Responses are unlimited on every plan. AI work is metered in credits, and credits are only consumed when a call actually produces a useful result — failed requests are never charged.

RequestCredits
Submissions with data (including /api/v1/inbox field detection)0
Submissions with raw1+, scaled by input size
/api/parse1+, scaled by input size
/api/v1/me, retries deduplicated by Idempotency-Key0

Your remaining credits appear in the dashboard top bar. Monthly allowances are listed on the pricing page.

Rate limits

The submissions endpoint is limited per form, not per plan. The default is 120 requests/minute, enforced over a rolling hourly window (so 7,200/hour). Form owners can change it in the form's API settings.

Every response carries your current budget:

Response headers
X-RateLimit-Limit: 7200
X-RateLimit-Remaining: 7199

Exceeding it returns 429 with { "error": "rate_limited", "retry_after_seconds": 60 } and a Retry-After header. Back off and retry; pair with an Idempotency-Key so retries stay safe.

Webhooks (outbound)

Webhooks send data out, they don't take data in

DodoForm webhooks notify your systems when a submission arrives. There is no inbound webhook URL to receive data — to push records into DodoForm, use the submissions endpoint above. Webhooks are available on every plan, including Free.

Configure them per form in the dashboard. This is the exact payload DodoForm sends:

POST to your endpoint
{
  "event": "submission.created",
  "form_id": "a1b2c3d4-...",
  "form_slug": "contact-us",
  "submission_id": "9c1f...",
  "created_at": "2026-07-28T09:15:00.000Z",
  "structured_data": { "name": "Jane Doe", "email": "jane@acme.com" },
  "metadata": { "source": "api", "key_id": "..." },
  "status": "auto_approved",
  "review": { "needs_review": false, "min_confidence": 1 }
}

Quick start

Drop-in handlers for your site's contact form. Only the API key is configured — no form id.

Node.js — contact form handler
// Only one secret to configure.
const KEY = process.env.DODOFORM_API_KEY; // df_live_...

export async function handleContactForm(fields) {
  const res = await fetch("https://www.dodoform.com/api/v1/inbox", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      // Makes retries safe — a repeat never double-files.
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      form: "Website contact",
      data: fields, // pass your form object straight through
    }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`DodoForm ${res.status}: ${err.error} — ${err.detail ?? ""}`);
  }
  return res.json();
}
Python
import os, uuid, requests

KEY = os.environ["DODOFORM_API_KEY"]  # df_live_...

def send_to_dodoform(fields: dict):
    res = requests.post(
        "https://www.dodoform.com/api/v1/inbox",
        headers={
            "Authorization": f"Bearer {KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={"form": "Website contact", "data": fields},
        timeout=15,
    )
    res.raise_for_status()
    return res.json()

Troubleshooting

Everything returns 404

You're probably calling /api/v1/submissions or /api/v1/forms — neither exists. The path must include your form ID: /api/v1/schemas/{FORM_ID}/submissions. Also check you're on www.dodoform.com; the bare domain doesn't serve the API.

Which key should I use?

If your code runs on a server, use a workspace key from /dashboard/api — it works everywhere, including /api/v1/inbox. If your site is static with no backend, use a browser key (df_pk_) instead, because a secret key would be visible in your page source.

I don't want to create a form or look up a UUID

Then use POST /api/v1/inbox with just your API key and a form name. It creates the form and detects fields from your JSON on the first call.

402 form_limit_reached from /api/v1/inbox

Each distinct form name creates a form, and forms count against your plan limit. Reuse one name per integration; the error response lists the form names you already have.

Some of my keys didn't become fields

Check ignored_keys in the response. Keys must start with a letter, stay under 40 characters, avoid reserved names like id, and fit within 60 auto-created fields per form.

400 invalid_form_id

Form ids are UUIDs like a1b2c3d4-e5f6-.... If yours is 24 hex characters or a readable slug, it came from somewhere else — copy the UUID from the dashboard URL.

403 key_schema_mismatch

A form key is locked to one form, and a workspace key only reaches forms in its own workspace. Check the form id matches the key.

Records arrive but fields are empty

The keys inside data must be field IDs, not labels. "Email Address" is a label; the ID is something like femail. The form's API tab lists every ID with a copy button.

Submissions show as needs_review

That's expected in raw mode when confidence is below the form's review threshold. Approve them in the dashboard, or send structured data to skip review entirely.

I want to create a form via the API

POST to /api/v1/inbox with a form name — the form is created on the first call and its fields are inferred from your payload. There's no endpoint for designing a form (adding logic, pages, or themes); do that in the dashboard.

403 origin_not_allowed

A browser key only works from the site origins listed on the key. Add the exact origin, including the scheme, at /dashboard/api — https://example.com and http://example.com are different, and *.example.com does not cover the apex domain.

403 raw_not_allowed_for_browser_key

Browser keys accept structured data only, because raw runs AI extraction and spends credits — and a public key could be abused to drain them. Send data from the browser, or call from your server with a secret key.

403 captcha_required or captcha_failed

This key has bot protection enabled. Render the Cloudflare Turnstile widget with DodoForm's site key and send its token as captcha_token. Tokens are single-use and expire in about five minutes, so fetch a fresh one per submission.

My browser key sends everything to the wrong form

A browser key is bound to one form chosen when you created it, and a form value in the request body is ignored on purpose — otherwise anyone could create forms from your page source. Create a separate key per destination form.

New fields aren't appearing from my browser key

Browser keys never add fields to an existing form, so a bot can't litter it with junk columns. The values are still stored and appear under "Additional data" on the submission. Add the field in the builder, or send the first payload with a secret key.

Still stuck?

Send us the endpoint you called and the exact error body — that's usually enough to spot it immediately.