> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.hodi.host/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# How do I create a form with Hodifly?

A contact form is quick to add to a page. Receiving it is another story: you usually need an external service, a subscription, or server-side code. **Hodifly forms** spare you all three.

You create a form in your HTML, mark it for Hodifly to handle, deploy, and the messages arrive in **cPanel → Hodifly → Forms**, with an email for every submission. Nothing else to install.

**What does this mean for you in practice?** Entries are stored **in your own account**, and there is **no limit on submissions** other than your disk quota. No plan to keep an eye on, no data held by a third party.

> **⚠️ Good to know up front:** Hodifly forms only work on **static sites**. A Node or Python application handles its own submissions in its code.

---

## 1. How do I create a form?

Add the `hodifly` attribute to your `<form>` tag, and give it a `name`:

```html
<form name="contact" data-hodifly method="POST">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button>Submit</button>
</form>
```

Four ways of writing it all work the same: **`hodifly`**, **`data-hodifly`**, **`netlify`**, and **`data-netlify="true"`**. A site already built for Netlify therefore works **with no changes at all**: you deploy it as-is.

### What happens at deployment

Hodifly detects your forms during the build and automatically:

1. injects a hidden `form-name` field — this is what identifies the form on receipt;
2. injects an invisible **bot trap** (*honeypot*): a field that only a bot would fill in;
3. generates a small script in the published site;
4. routes POST requests to that script.

You don't have to configure anything. The form appears in **Forms** as soon as it's deployed, **even with no entries yet** — handy for checking that it has been detected correctly.

### What does the visitor see after submitting?

They're sent back to the page they posted from, with `?hf=ok` added to the URL. It's up to you to display the thank-you message:

```html
<p id="merci" style="display:none">Thank you, your message has been received.</p>
<script>
  if (/[?&]hf=ok/.test(location.search)) document.getElementById('merci').style.display = '';
</script>
```

To land on a **different page**, point the form's `action` at it (`action="/merci/"`): the visitor will be redirected there after the entry is saved.

If the submission is rejected, the URL receives `?hf=err&hf_why=<reason>` (see the reasons table further down).

### What about AJAX?

If your form is submitted in the background by JavaScript, add the `Accept: application/json` header to the request: you get `{"ok":true}` back instead of a redirect, or `{"ok":false,"error":"captcha"}` if it's rejected.

---

## 2. Where can I find received messages?

In cPanel, open **Hodifly → Forms**. There you'll find:

* **the list of all your forms**, across every project: name, project, number of entries, date of the last one;
* for each row: **View** (entries), **Settings**, **Export to CSV**;
* on a form's page: **← Forms** to go back to the list, then **← Projects** to go back to the projects;
* **Clear All** to empty a form: be careful, this is irreversible.

---

## 3. What settings can I change?

Click **Settings** next to a form. Good news: settings are read **on every submission**. A change therefore takes effect immediately, **with no need to redeploy your site**.

### Emails

* **Email me every entry**: enabled by default. Uncheck it to stop receiving anything. Entries are still saved, and the webhook still fires.
* **Send entries to**: one or more addresses separated by commas. Left blank, Hodifly uses the project's notification address (or, failing that, the cPanel contact).

The email uses the same formatting as deployment notifications. Its **Reply-To** field points to the visitor's address as soon as the form contains an email field: you can reply straight from your inbox, no copy-pasting needed.

### Captcha

Choose **Cloudflare Turnstile**, **hCaptcha**, or **reCAPTCHA v2**, then paste the provider's **secret key**.

The division of labor is simple: **you add the provider's widget** to your form, **Hodifly verifies the token** server-side. If verification fails, the entry is rejected (`hf_why=captcha`). When in doubt, Hodifly rejects: a configured captcha is always enforced.

### File uploads

Enable **Allow file uploads**, then set a **maximum size** (in MB) and a **list of allowed extensions** (`pdf, png, jpg`).

Your form must include `enctype="multipart/form-data"`: this is the HTML attribute that lets a form carry files:

```html
<form name="contact" data-hodifly method="POST" enctype="multipart/form-data">
  <input type="file" name="piece-jointe" accept=".pdf,.png,.jpg">
  ...
</form>
```

Files are stored **outside the web root** - so they're never publicly accessible - and can be downloaded from the form's page. A disallowed extension returns `hf_why=filetype`, an oversized file returns `hf_why=filesize`.

The size is still capped by what PHP accepts on the server: the actual limit is shown below the setting.

### Outgoing webhook

A *webhook* is an automatic notification sent to another application whenever an event occurs — here, for every form entry. Useful for feeding a CRM, a team channel, or an internal tool.

Enter a **URL** (`https://…`) and, if you wish, a **signing key**. Every entry is sent there as JSON.

---

## 4. What is the webhook format?

Hodifly sends a `POST` request with `Content-Type: application/json` and a payload close to Netlify's:

```json
{
  "payload": {
    "id": "93ee97f9f1769bea",
    "form_name": "contact",
    "site_url": "https://exemple.re/",
    "created_at": "2026-07-15T04:11:11+00:00",
    "data": {
      "email": "jeanne@exemple.com",
      "message": "Bonjour",
      "ip": "203.0.113.10",
      "user_agent": "Mozilla/5.0 …"
    },
    "human_fields": {
      "Email": "jeanne@exemple.com",
      "Message": "Bonjour"
    },
    "ordered_human_fields": [
      { "title": "Email",   "name": "email",   "value": "jeanne@exemple.com" },
      { "title": "Message", "name": "message", "value": "Bonjour" }
    ],
    "files": [
      { "field": "piece-jointe", "name": "cv.pdf", "size": 20481, "stored": "20260715-041111-a1b2c3d4-cv.pdf" }
    ]
  }
}
```

| Champ | Contenu |
| ---- |
| `id` | Unique identifier of the entry |
| `form_name` | Your form's `name` |
| `site_url` | The URL of the site that received the submission |
| `created_at` | UTC date/time (ISO 8601) |
| `data` | The raw fields, plus `ip` and `user_agent` |
| `human_fields` | The same fields with a readable label |
| `ordered_human_fields` | Same, keeping the form's field order |
| `files` | The submitted files (the content stays in your account) |

### Verifying the signature

If you've set a key, the request carries the following header:

```
X-Hodifly-Signature: sha256=<HMAC-SHA256 of the raw body, using your key>
```

This signature lets you verify that the request really comes from your site, not from a third party. Verify it against the **raw body** of the request, **before** any JSON decoding. Example in PHP:

```php
$raw      = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, 'YOUR_KEY');
if (!hash_equals($expected, $_SERVER['HTTP_X_HODIFLY_SIGNATURE'] ?? '')) {
    http_response_code(403); exit('invalid signature');
}
$entry = json_decode($raw, true)['payload'];
```

---

## 5. Why was a submission rejected? (`hf_why`)

| Valeur | Signification |
| ---- |
| `captcha` | The captcha wasn't validated |
| `filetype` | Disallowed file extension |
| `filesize` | File exceeds the form's maximum size |
| `toolarge` | Submission exceeds what PHP accepts on the server |
| `upload` | File upload failed |

---

## 6. Is my data protected?

* **Anti-bot.** An invisible *honeypot* field is injected automatically. A bot that fills it in gets a normal response, but nothing is saved or sent — it never knows it was filtered out.
* **CSRF.** A submission coming from another site is rejected: Hodifly checks the origin of the request, which prevents a third party from posting on your behalf.
* **No open relay.** The handler only accepts forms that were actually detected during the last deployment.
* **Storage.** Entries and files are stored outside the web root, with `0600` permissions, and survive both deployments and restores.
* **Privacy.** Everything stays on your account. Entries never pass through Hodifly.

---

## 7. What are the limitations?

* **Static sites only.** A Node or Python application handles its own submissions in its code.
* A form removed from your HTML no longer accepts submissions, but **keeps** the entries already received.
* File contents aren't attached to emails: they're downloaded from cPanel.
* No Akismet-style spam filtering: the honeypot and captcha cover most needs.

---

## 8. If something goes wrong

| Symptôme | À vérifier |
| ---- |
| The form doesn't appear in **Forms** | Is the `hodifly`/`netlify` attribute present? Does the `<form>` have a `name`? Has the site been redeployed? The deployment must be **static**. |
| "Unknown form" | The form didn't exist at the last deployment: redeploy. |
| The file isn't saved | Are file uploads enabled in **Settings**? Is the extension allowed? Does the form have `enctype="multipart/form-data"`? |
| No email | Is the **Email me every entry** box checked? Is the address valid? |
| The webhook isn't receiving anything | The URL must be `http(s)://` and reachable from the server. |
