# Contact forms (https://coss.com/solutions/contact-forms)

> coss.com is a backend for contact and lead forms. Post plain HTML to an endpoint, and it screens spam, emails your team and adds leads to your contacts.

## The problem

A contact form is a few inputs on a page, and it still needs a server, a spam filter and somewhere for the messages to go.

Static sites have nowhere to post a form. Adding a function for it means parsing the post, validating fields, keeping bots out, emailing the right person and getting the lead into your CRM or mailing list, and every public form attracts spam the day it goes up.

coss.com does this on one key: [Forms](/products/forms) gives each form an endpoint, [Risk](/products/risk) screens every submission, [email](/products/email) tells your team, and [contacts](/products/marketing-email) keep the leads you can write to later.

## How it works

Create a form, point your HTML at its endpoint, and accepted submissions come to you.

```ts twoslash
import { Coss } from "coss";
const coss = new Coss();
declare const leadsAudienceId: string;
// ---cut---
const { data: form } = await coss.forms.create({
  name: "Contact us",
  allowed_origins: ["https://www.example.com"],
  fields: [
    { name: "email", type: "email", required: true },
    { name: "first_name", type: "text" },
    { name: "company", type: "text", max_length: 100 },
    { name: "message", type: "textarea", max_length: 5000 },
  ],
  // Your team gets an email, from your domain, and replies go to the submitter.
  notifications: { recipients: ["sales@example.com"], from: "forms@example.com" },
  // The submitter becomes a contact in your leads audience.
  contacts: {
    enabled: true,
    audience_ids: [leadsAudienceId],
    first_name_field: "first_name",
    properties: { company: "company" },
  },
});
// form?.endpoint: https://…/f/form_01k…
```

```html
<form action="https://…/f/form_01k…" method="post">
  <input type="email" name="email" required>
  <input type="text" name="first_name">
  <input type="text" name="company">
  <textarea name="message"></textarea>
  <input type="hidden" name="_next" value="https://www.example.com/thanks">
  <!-- The honeypot: people leave it empty, bots fill it in. -->
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
  <button>Send</button>
</form>
```

That's a working form with no JavaScript. To submit without a page load and show errors per field, add `attachForm`, which also sends a device token so Risk recognizes the browser:

```ts twoslash
import { attachForm } from "coss/forms";
// ---cut---
const form = document.querySelector<HTMLFormElement>("#contact");
if (form) {
  attachForm(form, {
    onResult(result) {
      if (result.ok) console.log("Thanks!");
      else if (result.errors) for (const error of result.errors) console.log(error.field, error.message);
    },
  });
}
```

To send leads to your CRM, handle the webhook:

```ts twoslash
import { verifyWebhook } from "coss";
declare const secret: string;
declare const request: Request;
declare function createLead(email: string | null, fields: Record<string, string | string[]>): Promise<void>;
// ---cut---
const event = await verifyWebhook(await request.text(), request.headers, secret);
if (event.type === "form.submission.received") {
  // Submitted values are untrusted input.
  await createLead(event.data.submitter_email, event.data.data);
}
```

Read more in the [Forms guide](/docs/forms), [HTML forms](/docs/forms/html-forms) and [destinations](/docs/forms/destinations).

## What you get

Collect, screen and deliver submissions, from plain HTML, a script or a page coss.com hosts.

### Any HTML form
The endpoint takes the URL-encoded and multipart posts browsers send, and JSON. Define `fields` to validate types and required values, or leave them out and take any field. See [HTML forms](/docs/forms/html-forms).

### Spam stays out
A honeypot, then a Risk assessment with your own [rules](/docs/risk/rules) and [lists](/docs/risk/lists-and-feedback), a content check, and an optional Turnstile or hCaptcha. Spam is answered like a success, so bots learn nothing. See [spam filtering](/docs/forms/spam-filtering).

### False positives come back
What Risk wants reviewed waits as `held`. Release it or anything marked `spam`, and it's delivered as if it had been accepted, and Risk learns from it. If Risk can't be reached, submissions are delivered rather than lost.

### Notifications your team can answer
Up to 5 recipients get an email per accepted submission, with every field and a link to it in the dashboard, and a reply goes to the submitter. They come from your verified domain, or from coss.com as "form name via coss.com".

### Leads in your contacts
Submitters become [contacts](/docs/email/contacts), with their audiences, topics and properties, ready for [broadcasts](/products/marketing-email). Double opt-in can make new contacts confirm first. See [double opt-in](/docs/forms/double-opt-in).

### Webhooks for everything else
`form.submission.received` and `form.submission.flagged` reach your [webhooks](/docs/webhooks), signed, with what was submitted. Each destination retries on its own if it fails.

### Hosted pages
Turn on `hosted` and the endpoint also serves the form as a page rendered from its fields, to link to or embed, with no JavaScript needed. See [hosted pages](/docs/forms/hosted-pages).

### Confirmed addresses and attachments
`submitter.verify` holds a submission until the submitter clicks the link emailed to them. [File uploads](/docs/forms/file-uploads) take attachments, served only as downloads. They aren't scanned for malware, so treat them as untrusted.

## Specifications

The limits a submission is checked against. The docs list each one in context.

- Encodings: URL-encoded, multipart, JSON
- Fields per submission: 100, 10,000 characters a value
- Text per submission: 64 KB
- Notification recipients: up to 5
- Allowed origins: up to 10, https
- Submissions per form: 10 a second, bursts of 50
- Submissions per client IP: 20 a minute
- Files: 3 by default, up to 10, 10 MB each
- Accepted submissions kept: 90 days
- Spam and unconfirmed kept: 14 days

## Pricing

You pay per accepted live submission, and for the files it carries. Spam, held and unconfirmed submissions cost nothing unless you release them or the submitter confirms.

| Product | Rate | |
| --- | --- | --- |
| Email | $0.40 per 1,000 recipients for the first 1,000,000; $0.30 per 1,000 recipients from 1,000,001 to 10,000,000; $0.22 per 1,000 recipients beyond 10,000,000 | |
| Form submissions | $1.00 per 1,000 submissions | |
| Form uploads | $0.25 per 1,000 MB | The files of accepted submissions, kept 90 days; rounded up to the MB per submission |

The Free plan includes 1,000 form submissions a month and 1,000 MB of form uploads a month. Notification emails sent from your own domain are billed like any email; notifications sent "via coss.com" are not. Sandbox forms are free. See [pricing](/pricing) for the plans.

## Frequently asked questions

### How do I add a contact form to a static site?

Create a form with coss.forms.create() or in the dashboard, then set your HTML form's action to its endpoint. coss.com stores each submission, screens it for spam and sends accepted ones to your team's inbox, your webhooks and your contacts. You write no server code.

### How does coss.com keep spam out of a contact form?

A hidden honeypot field catches simple bots at once. Every other submission is a Risk assessment of the submitter's email, IP address, browser and the text they wrote, with your own rules and lists, plus a content check for spam, phishing, malware and fraud and an optional Turnstile or hCaptcha. Spam is answered exactly like a success, so bots learn nothing.

### What if a real message is marked as spam?

Find it on the form's Spam or Held tab and release it, in the dashboard or with coss.forms.submissions.release(). It is delivered as if it had been accepted, and Risk records the assessment as legitimate, so it learns from the mistake.

### Will I get an email for each submission?

Yes. Up to 5 recipients get an email per accepted submission with every field, and a reply goes straight to the submitter. In live environments, recipients must be members of your organization or addresses on its verified domains.

### How do I send leads to my CRM?

Subscribe a webhook endpoint to form.submission.received. Each one is signed and carries the submitted fields, so your handler can create the lead. Treat the values as untrusted input.

### Can submitters join my mailing list?

Yes. With contacts turned on, the submitter becomes a contact of the environment, added to the audiences and topics you choose, with fields mapped to contact properties. Double opt-in can make new contacts confirm their address first.

### Do I have to write the HTML?

No. Turn on a hosted page and coss.com renders the form from its field definitions, at a link you can share or embed in an iframe. Hosted pages are on coss.com's forms domain, since custom domains for them aren't available.

### Is there a drag-and-drop form builder?

No. You define a form's fields in code or write your own HTML. Drag-and-drop building and conditional or multi-page forms aren't available.
