# Moving from Formspree

Part of the sendm8 docs: https://sendm8.com/docs#formspree. Index for AI assistants: https://sendm8.com/llms.txt

sendm8 reads the same field names as Formspree, so a plain HTML form moves with a one-line change: the `action` URL. Code that sends forms with JavaScript needs a small change to how it reads errors.

Moving a form

1. **Get a sendm8 endpoint.** Create a form in the [dashboard](https://sendm8.com/app) and copy its URL, or skip signing up and use `https://sendm8.com/f/you@example.com`.
2. **Swap the URL** everywhere the form posts to `https://formspree.io/f/…`.
3. **Send a test submission** and check it arrives where you expect. Then add Discord, Slack, Telegram or webhooks if you like.

**After: `contact.html`**

```html
<form action="https://sendm8.com/f/k3x9q2m7ab" method="POST">
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <input type="hidden" name="_subject" value="New enquiry">
  <input type="text" name="_gotcha" style="display:none">
  <button>Send</button>
</form>
```

**Before: `contact.html`**

```html
<form action="https://formspree.io/f/xyzabcde" method="POST">
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <input type="hidden" name="_subject" value="New enquiry">
  <input type="text" name="_gotcha" style="display:none">
  <button>Send</button>
</form>
```

### What maps to what

| Formspree | sendm8 |
| --- | --- |
| `https://formspree.io/f/{id}` | `https://sendm8.com/f/{id}`, or `https://sendm8.com/f/{your email}` with no account |
| `_replyto` or `email`, `_subject`, `_next`, `_gotcha`, `_cc` | The same. `_next` must be on one of the form’s allowed domains, and `_cc` only copies addresses verified in your account. |
| Other underscore fields | Ignored and not stored. |
| JSON success: `{ ok: true }` | `{ ok: true, id }` |
| JSON errors: an `errors` array | `{ ok: false, error: { code, message } }` with a matching HTTP status. [AJAX / fetch](https://sendm8.com/docs/ajax). |
| `@formspree/react` (`useForm`) | A few lines of `fetch`, below. The package only talks to Formspree. |
| `@formspree/ajax` and `data-fs-*` attributes | The helper script and `data-sendm8` attributes. [AJAX without writing JavaScript](https://sendm8.com/docs/ajax-helper). |

### JavaScript and React

**fetch: `contact.js`**

```js
const res = await fetch("https://sendm8.com/f/k3x9q2m7ab", {
  method: "POST",
  body: new FormData(form),
  headers: { Accept: "application/json" },
});
const result = await res.json();

if (result.ok) {
  showThanks();
} else {
  // Formspree: result.errors.map((e) => e.message)
  showError(result.error.message);
}
```

**React: `ContactForm.jsx`**

```jsx
// Before: import { useForm, ValidationError } from "@formspree/react";
import { useState } from "react";

export function ContactForm() {
  const [state, setState] = useState({ submitting: false, succeeded: false, error: null });

  async function handleSubmit(event) {
    event.preventDefault();
    setState({ submitting: true, succeeded: false, error: null });
    const res = await fetch("https://sendm8.com/f/k3x9q2m7ab", {
      method: "POST",
      body: new FormData(event.currentTarget),
      headers: { Accept: "application/json" },
    });
    const result = await res.json();
    setState({ submitting: false, succeeded: result.ok, error: result.ok ? null : result.error.message });
  }

  if (state.succeeded) return <p>Thanks! It's on its way.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      {state.error && <p role="alert">{state.error}</p>}
      <button disabled={state.submitting}>Send</button>
    </form>
  );
}
```

### Migrating with an AI assistant

These docs are also published as Markdown for AI assistants: [/llms.txt](https://sendm8.com/llms.txt) lists every section, and [/llms-full.txt](https://sendm8.com/llms-full.txt) has all of them in one file. Create your sendm8 forms first (an assistant can’t do that for you), then give it a prompt like this in the app you’re migrating.

`prompt.txt`

```
Migrate this app's forms from Formspree to sendm8. Read https://sendm8.com/llms-full.txt first.

Endpoint mapping (Formspree → sendm8):
- https://formspree.io/f/xyzabcde → https://sendm8.com/f/k3x9q2m7ab

1. Find every Formspree usage: "formspree.io", "@formspree/react", "@formspree/ajax",
   data-fs-* attributes, and environment variables holding Formspree form IDs.
2. HTML forms: change only the action URL. Field names stay the same.
3. JavaScript submissions: POST to the sendm8 URL with "Accept: application/json".
   Success is { ok: true, id }. Failure is { ok: false, error: { code, message } }:
   show error.message. Update any code that reads Formspree's errors array.
4. Replace @formspree/react and @formspree/ajax with fetch (or the sendm8 helper script),
   keeping the same loading, success and error states. Then remove the packages.
5. Don't change styling, copy or field names.
6. List every file you changed, and anything you couldn't migrate.
```

> **Worth knowing**
>
> Uploads are capped at 5 MB a file. If your Formspree forms only accept submissions from certain sites, add those domains under **Form settings → General → Allowed domains**.
