# Doorman > Doorman is machine-learning spam, fraud and abuse protection for website forms: contact, demo request, quote, sign-up, checkout and comments. It stops sales pitches, SEO spam, bots, fake and throwaway sign-ups, card testing and abusive comments, forwards only the real submissions, and explains every drop with the sentence (or field) that gave it away, rescuable in one click. No captcha: real visitors never see anything. Each submission is judged in about 200 ms, inline, before the thank-you page loads. This file is for coding agents (Claude Code, Codex, Cursor, etc.) setting Doorman up for a user. Everything below can be done without a browser. Base URL: https://withdoorman.com Human docs: https://withdoorman.com/docs MCP server: https://withdoorman.com/mcp (Streamable HTTP) ## Fastest path: MCP If you can add MCP servers, do this and use the tools (doorman_signup, doorman_get_install_snippet, doorman_update_form, doorman_test_submission): claude mcp add --transport http doorman https://withdoorman.com/mcp (Codex: add to ~/.codex/config.toml: [mcp_servers.doorman] url = "https://withdoorman.com/mcp". Once you have a key, add the header Authorization: Bearer , or pass api_key to each tool.) ## Setup steps (plain HTTP) 1. Find the project's forms. Look for
elements, form libraries (react-hook-form, Formik, Webflow/Framer exports), and backend routes that receive contact/demo/lead submissions (e.g. /api/contact). Note what the business sells from the README or landing copy. 2. Ask the user for the email address real submissions should go to. Then create the account: curl -X POST https://withdoorman.com/v1/signup -H "Content-Type: application/json" -d '{ "email": "owner@example.com", "site_url": "example.com", "site_context": "Example sells invoicing software to small businesses in the US.", "agent": "claude-code" }' Response: { "api_key": "dm_live_...", "form": { "id": "f_...", "action_url": "https://withdoorman.com/f/f_...", ... }, "next_steps": [...] } The user is emailed a link to set a dashboard password; that link also confirms their email. Until they click it the account has 100 checks and email forwarding is off (Slack and webhooks work straight away), so tell them to click it. Save api_key as DOORMAN_KEY in the project's .env (make sure .env is gitignored). Never commit it. If the email already has an account, ask the user for an API key from https://withdoorman.com/app#/settings instead. site_context matters: Doorman judges each submission against it ("is this a person who wants what this business offers?"). One or two concrete sentences: what is sold, to whom. 3. Integrate. Pick ONE per form: a) Static HTML / any site builder (no key, no backend): set the form's action and method. ...
Keep existing field names. Doorman detects email (email, e-mail, your_email), name (name, full_name, first_name) and message (message, comments, body, details, inquiry, question, notes, description) by name; anything else is kept as an extra field. Add data-doorman-ajax to the form to submit without a page reload. A hidden _next field (same site only) or the form's redirect_url sets where the visitor lands; default is https://withdoorman.com/thanks. b) React / SPA: POST FormData (or JSON) from the client to the action URL with Accept: application/json. The response is always {"ok": true} whatever the verdict. Get a component: GET https://withdoorman.com/v1/forms/f_.../snippet?platform=react c) The form already posts to the project's backend: call the API from the handler and act on the result. POST https://withdoorman.com/v1/check Authorization: Bearer $DOORMAN_KEY { "form": "f_...", "email": "...", "name": "...", "message": "...", "signals": { "ip": "...", "seconds_on_page": 42, "honeypot_filled": false } } -> { "id": "sub_...", "route": "deliver"|"hold"|"drop", "action": "deliver"|"hold"|"drop", "p_real": 0.97, "reason": null|"pitch"|"automated"|"fraud", "evidence": "the sentence that gave it away" } Act on `action` (it accounts for watch mode). drop: show the normal thank-you but don't save/notify. hold: save it flagged for review. deliver: normal path. Never tell the visitor the verdict. Wrap the call so a Doorman outage treats the submission as hold, not drop (the doorman-client npm package does this: `new Doorman(process.env.DOORMAN_KEY, { baseUrl: "https://withdoorman.com" }).check({...})`). Snippets: GET https://withdoorman.com/v1/forms/f_.../snippet?platform=nextjs|express|python|ruby|elixir|curl Checkout / orders: kind "checkout" with { "order": { "items": "...", "total_usd": 120 }, "signals": { "card_country", "ip_country", "attempts", "device_orders_last_hour", "account_age_days", "name_match", "card_type", "shipping" } }. 4. Destinations (where real submissions go) for hosted forms. Every form forwards to the account's email by default (pass email_to: "" to turn that off). Add Slack or a webhook if the user wants: PATCH https://withdoorman.com/v1/forms/f_... { "slack_url": "https://hooks.slack.com/services/...", "webhook_url": "https://...", "email_to": "a@x.com, b@x.com" } Email forwarding only goes to confirmed addresses: the account's own email once confirmed, and any other address after its owner clicks a confirmation email Doorman sends them. Webhook and Slack URLs must be public (private and internal addresses are refused). Webhooks are signed: header Doorman-Signature: t=,v1=.")>. webhook_secret is on the form object. Failed deliveries are retried after 1m, 5m, 30m, 2h, 12h. Check they work: POST https://withdoorman.com/v1/forms/f_.../test-delivery 5. Verify and show the user. Both are free (up to 100 test submissions a day) and not forwarded: POST https://withdoorman.com/v1/forms/f_.../test { "message": "Hi, we run a 12-person dental clinic and spend a day a week chasing invoices. Can we see a demo?" } -> deliver POST https://withdoorman.com/v1/forms/f_.../test { "message": "We help SaaS founders fill their pipeline with done-for-you outreach. 15 min chat?" } -> drop Tell the user: the action URL, where submissions go, that drops are visible and rescuable at https://withdoorman.com/app, and that they'll get an email to set a password. ## Options worth knowing - mode: "watch" delivers everything and tags would-be drops. Suggest it if the user is nervous; switch to "enforce" later. - deliver_at (default 0.8) and drop_below (default 0.2) are the P(real) thresholds. Between them is "hold": forwarded flagged as held (forward_holds, default true) and queued for review. - allow_list / block_list: emails or domains (acme.com, *.acme.com) always delivered / always dropped, without a check. - kind: lead (contact/demo/quote/sales, default; pitches, spam, bots), signup (fake and throwaway accounts), checkout (card testing, stolen cards), comment (promotion, link spam, harassment, hate, threats). One form per distinct form on the site: POST https://withdoorman.com/v1/forms { "name": "Demo request", "kind": "lead", "site_context": "..." } - Nothing is silently dropped: over the monthly quota or when judging fails, submissions are held and forwarded unjudged, flagged as held. A flood from one IP (over 30 posts a minute to one form) is stored as held in the dashboard, not forwarded. ## API reference (all need Authorization: Bearer dm_live_... except signup) - POST /v1/signup { email, site_url, site_context, form_name?, agent? } - GET /v1/me - GET /v1/forms | POST /v1/forms | GET/PATCH/DELETE /v1/forms/{id} - GET /v1/forms/{id}/snippet?platform=html|ajax|react|nextjs|express|curl|python|ruby|elixir|webflow|framer|wordpress - POST /v1/forms/{id}/test { message, email? } - POST /v1/forms/{id}/test-delivery - POST /v1/check { form? | kind? + context?, email, name, message, fields?, signals?, order?, forward? } - GET /v1/submissions?form=&route=&review=true&before=&limit= - GET /v1/submissions/{id} - POST /v1/submissions/{id}/label { label: "real"|"spam" } Errors are JSON { "error": "human-readable message" } with 4xx/5xx status. 429 means slow down. ## Pricing Free: 1,000 checks/month. Pro: $12/month, 25,000. Business: $49/month, 250,000. Test submissions are free.