Create a form with no account
Draft. This page describes the behaviour as implemented. The production host names are not live yet. The claim email itself is sent by the email step, which is not built yet: until then the claim link is returned to the agent and mail is queued, never sent.
How it works
An agent that has only an email address can make a working form. The form and its key are real, but the key is weak and sleeps until the owner of that address claims it. Claiming changes nothing for the website: the same key and the same form address keep working.
| Time | State | What happens |
|---|---|---|
| 0 to 72 hours | unclaimed, awake | The key works within its caps. Real submissions are stored and held, up to a bounded allowance; nothing is delivered. |
| 72 hours to day 90 | asleep | The key and the form address keep existing. Real submissions are still stored (up to the allowance) and held. REST answers 403 claim_required with the claim link; over MCP the only tool is resend_claim_email. |
| claimed, any time before day 90 | claimed | The same key becomes a normal account key. Claiming alone does not enable delivery: held leads stay held and are released, in order, and counted only when the owner's plan or trial entitles delivery, within that plan's allowance. |
| day 90 | retired | Data is purged. The key answers 410 key_gone and is never issued again. Create a new key. |
Create
Create a form and a key with no account
POST /api/forms
Content-Type: application/json
{
"email": "owner@example.com",
"name": "Contact"
}
-> 201: form, apiKey, claimUrl and replaySecret (shown once)
Ask for the claim email again
POST /api/claim/resend
Authorization: Bearer <the unclaimed key>
-> 202: queued: the mail goes only to the address the key was created for
Claim a key (a signed-in person, whose verified email is the claim address)
POST /api/claim
Authorization: Bearer <a signed-in person's token>
Content-Type: application/json
{
"token": "clm_Ab3dE5gH7jK9mN2p"
}
-> 200: formId, siteId and formKey: the same key and form address are now the person's
The answer holds the form key, the apiKey (cf_agent_..., shown once), a claimUrl and a replaySecret. The claim link is also queued for the address. Over MCP, call create_form with an email and no key.
- At most one new key per hour per network and per machine (send a stable
X-CF-Client-Id; without one the user-agent identifies the machine), and three a day per network. Over the limit: 429 withRetry-After. - One waiting key per email address (asleep counts) until it is claimed or retired: 409
claim_pending. - A global daily ceiling is the last backstop (503
capability_paused).
If the answer was lost, send the same call again with the replaySecret the first answer carried and the same email: the same key comes back. Without the secret it never does, even for someone behind the same network with the same user-agent.
# the first answer was lost: send the replaySecret it carried (and the same email)
curl https://contactforms.dev/api/forms \
-H "Content-Type: application/json" \
-H "X-CF-Client-Id: <a stable random id your tool keeps>" \
-d '{"email":"owner@example.com","replaySecret":"rps_..."}'
While it waits
- One form (a second one is refused with a fix). Real submissions are stored and held pending claim and entitlement, up to a bounded allowance. Nothing is delivered and nothing counts toward a plan before someone owns the key. When the allowance is used up the answer is 429
unclaimed_cap. - The only address that can ever hear about the form is the one it was created for, saved unverified. Mail about the key is limited to one a minute and three a day per address.
- A test lead (
send_test) goes only to that address. - No export. Leads stay held until the claim: they are never delivered or counted before someone owns them.
Claim it
The owner opens the claim link (/c/<code>) while signed in with the address the key was created for, or pastes the key on the Agents page. The address must be verified in their account; the link alone is not enough, because the agent holds it too. Opening the link changes nothing; the button claims.
Create a form and a key with no account
POST /api/forms
Content-Type: application/json
{
"email": "owner@example.com",
"name": "Contact"
}
-> 201: form, apiKey, claimUrl and replaySecret (shown once)
Ask for the claim email again
POST /api/claim/resend
Authorization: Bearer <the unclaimed key>
-> 202: queued: the mail goes only to the address the key was created for
Claim a key (a signed-in person, whose verified email is the claim address)
POST /api/claim
Authorization: Bearer <a signed-in person's token>
Content-Type: application/json
{
"token": "clm_Ab3dE5gH7jK9mN2p"
}
-> 200: formId, siteId and formKey: the same key and form address are now the person's
Claiming the key does not by itself start delivery or give a fresh allowance: delivery needs an active plan or trial. If the person's plan has no site left for it, the claim is refused with plan_limit_sites and nothing is lost until day 90.
Lost the email
POST /api/claim/resend (or the MCP tool resend_claim_email) asks for the claim email again. It goes only to the stored address; nothing in the request can name another. At most three a day per key. A sleeping key can still call it. Until email sending ships, the answer says delivery: "pending_email_sending".
After 90 days
The leads, form and site are deleted, the claim address is removed, and the key becomes a tombstone that answers 410. It is never re-issued; a new request for the same address makes a different key.