# Troubleshooting

Find what's wrong and get your form working again. Each section starts from what you see, shows how to check
it, and gives the fix. Formerie returns stable codes, and this page uses their exact names.

The sections:

- [My Email Didn't Arrive](#my-email-didnt-arrive)
- [A Submission Was Rejected](#a-submission-was-rejected)
- [The Form Isn't Showing](#the-form-isnt-showing)
- [A CORS Or Origin Error](#a-cors-or-origin-error)
- [Verification Failed](#verification-failed)

## My Email Didn't Arrive

**Symptom:** someone filled in the form, and the email never showed up.

**How to check:** open **Deliveries** in the app and find the response by form and date. Each row has a status, and
a diagnostic with a code, a summary, and a next step. The same details are on the response itself under
**Submissions**. Then find what you see in this table.

| What you see | What it means | What to do |
| --- | --- | --- |
| No response in **Submissions** | It was refused before it was stored | See [A Submission Was Rejected](#a-submission-was-rejected) |
| A response with no delivery | The form had no recipients, so it was only stored | Add a recipient under **Delivery**. New responses get email from then on. Earlier ones were never sent, and a retry doesn't send them |
| **Queued** | Waiting for its attempt | Wait a moment |
| **Retrying** | A temporary problem with the provider. Formerie tries up to 5 times, 60 seconds apart | Wait, unless the diagnostic is `DELIVERY_STALLED` |
| **Failed** | The email couldn't be sent | Find the code in the next table, fix it, then choose **Retry delivery** |
| **Delivered** | The provider accepted the email | See [Delivered, But Not In The Inbox](#delivered-but-not-in-the-inbox) |

### Delivery Diagnostic Codes

| Code | What the app says | Fix |
| --- | --- | --- |
| `DELIVERY_RECIPIENTS_UNAVAILABLE` | No active verified recipients are configured for this form | Have the recipient confirm by email, or add another one, under **Delivery** |
| `DELIVERY_SENDER_UNAVAILABLE` | The selected sender identity is not active or verified | Confirm the sender under **Senders**, or set **Send from** back to the Formerie default |
| `DELIVERY_CREDENTIALS_MISSING` | The delivery provider is missing required credentials | Reconnect the provider under **Destinations** |
| `DELIVERY_PROVIDER_NOT_FOUND` | The configured email provider could not be used | Review the provider under **Destinations** and on the **Delivery** tab |
| `DELIVERY_PROVIDER_UNREACHABLE` | The delivery provider could not be reached | Check the provider's status, then retry |
| `DELIVERY_PROVIDER_ERROR` | The provider did not accept the delivery | Check the provider's status and your settings with them, then retry |
| `DELIVERY_FAILED` | Delivery failed before the provider accepted the message | Review the sender, recipients, and provider, then retry |
| `DELIVERY_STALLED` | Delivery stopped retrying after a temporary error | Open the response and retry |
| `DELIVERY_PROVIDER_UNREACHABLE_RETRYING` | The delivery provider could not be reached | Nothing yet. Formerie will try again |
| `DELIVERY_PROVIDER_TEMPORARY_ERROR` | The provider returned a temporary delivery error | Nothing yet. Formerie will try again |
| `DELIVERY_QUEUED`, `DELIVERY_WAITING_FOR_ATTEMPT` | Delivery is waiting for an attempt | Nothing yet |
| `DELIVERY_ACCEPTED`, `DELIVERY_COMPLETED` | The provider accepted the email | See the next section if it still hasn't arrived |

### Delivered, But Not In The Inbox

**Delivered** means the provider took the email. It doesn't prove it reached the inbox.

1. Ask the recipient to check their spam folder.
2. If you use your own email provider, search its logs for the message.
3. Open the form's **Delivery** tab, and make sure that address is on the form.
4. Open **Recipients**, and look at that address. A **Pending**, **Disabled**, or **Suppressed** recipient gets nothing.
   A suppressed address usually bounced or marked a message as spam.

### Only Some People Got It

A recipient with a condition only gets the responses that match it. Compare the condition on the **Delivery** tab with the
answers in the response. If a condition leaves nobody to send to, the response is kept and no email goes out, and that
isn't counted as a failure.

### Retry A Delivery

Fix the cause first, because most failures repeat otherwise. Then open the response under **Submissions** and choose
**Retry delivery**. It sends real email with the form's current delivery settings, and doesn't change the response.

| Code | Meaning |
| --- | --- |
| `DELIVERY_NOT_RETRYABLE` | This delivery can't be retried right now, for example because it was already delivered |
| `DELIVERY_ATTEMPT_IN_PROGRESS` | An attempt is running. Wait for its result |
| `QUEUE_FAILED` | The retry couldn't be queued. Try again shortly |

A delivery that shows `EMAIL_CEILING_REACHED` was stopped because the account reached its monthly
ceiling for email sent through Formerie. The response is kept. Retry it from the 1st of the next month,
or [write to us](https://formerie.com/contact/) if you need more sooner.

### A Test Email Won't Send

| Code | Cause | Fix |
| --- | --- | --- |
| `FORM_DELIVERY_TEST_NOT_READY` | A sender or recipient isn't ready, or the form has no recipients | Follow the items under **Delivery readiness** |
| `FORM_DELIVERY_TEST_NO_RECIPIENTS` | The sample answers match no recipient | Change the sample answers under **Preview**, or review recipient conditions |
| `FORM_DELIVERY_TEST_RATE_LIMITED` | You can send one test per minute | Wait a minute |
| `FORM_DELIVERY_TEST_SEND_FAILED` | The provider refused it | Check the sender and provider, then try again |

## A Submission Was Rejected

**Symptom:** the visitor sees an error, or your code gets `ok: false`.

**How to check:** a refused submission isn't stored, so it never appears under **Submissions**. Read the code in the response
instead, in your browser's network panel or from `result.error.code` in the SDK. It looks like this:

```json
{
  "ok": false,
  "error": {
    "code": "SPAM_REJECTED",
    "message": "The submission was rejected."
  }
}
```

The server runs its checks in order and stops at the first one that fails: origin, availability, rate limit on public routes,
response window, size, format, repeat send, submission limits, spam checks, human verification, answers, and quota.

| Code | HTTP status | Cause | Fix |
| --- | --- | --- | --- |
| `VALIDATION_FAILED` | 422 | An answer failed a check | See [Failed Answers](#failed-answers) |
| `SPAM_REJECTED` | 403 | A spam check caught it | See [Rejected As Spam](#rejected-as-spam) |
| `TURNSTILE_FAILED` | 403 | Human verification failed, with any provider | See [Verification Failed](#verification-failed) |
| `RATE_LIMITED` | 429 | Too many submissions in a short time | See [Too Many Submissions](#too-many-submissions) |
| `FORM_QUOTA_REACHED` | 409 | The form hit its **Maximum accepted submissions** | Raise or clear it under **Submission limits**, in **Settings** |
| `DUPLICATE_SUBMISSION` | 409 | This `metadata.idempotencyKey` was already accepted | Treat it as a success. See [Repeat Sends](#repeat-sends) |
| `FORM_NOT_OPEN` | 403 | Before the **Response window** opens | Change the window on the **Publish** tab |
| `FORM_CLOSED` | 403 | The form is closed, or its window has ended | Change **Public access** or the window on the **Publish** tab |
| `FORM_MAINTENANCE` | 503 | **Public access** is on **Maintenance** | Set it back to **Public** |
| `ACCOUNT_STORAGE_HOLD` | 403 | The account stores far more than normal use, so its forms paused new submissions | [Write to us](https://formerie.com/contact/). Everything already received is safe |
| `FORM_NOT_FOUND` | 404 | Wrong address, or the form isn't published | See [The Form Isn't Showing](#the-form-isnt-showing) |
| `FORBIDDEN_ORIGIN` | 403 | The browser's site isn't an allowed origin | See [A CORS Or Origin Error](#a-cors-or-origin-error) |
| `BAD_REQUEST` | 400 or 413 | The body has no `fields` object, or is over 100,000 bytes | Send `{ "fields": { ... } }`, and shorten large answers |
| `INVALID_JSON` | 400 | The body isn't valid JSON | Fix what your code sends |
| `QUEUE_FAILED` | 503 | The email couldn't be queued | Ask the visitor to try again shortly |
| `INTERNAL_ERROR` | 500 | Something went wrong on our side | Try again shortly |

### Failed Answers

`VALIDATION_FAILED` lists each problem in `error.fields` and `error.issues`, with a path, a code, and a message:

```json
{
  "ok": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The submitted form contains invalid fields.",
    "fields": {
      "contact.email": [
        {
          "path": "contact.email",
          "code": "FIELD_EMAIL_INVALID",
          "message": "Invalid email address.",
          "rule": "email",
          "params": {}
        }
      ]
    },
    "issues": [
      {
        "path": "contact.email",
        "code": "FIELD_EMAIL_INVALID",
        "message": "Invalid email address.",
        "rule": "email",
        "params": {}
      }
    ]
  }
}
```

Show each message next to its field. Hosted pages and embeds do this for you. The codes are listed by field type in the
[Form Definition Reference](/guides/form-reference/), and your own validators return the `code` you gave them.

A field that's required but hidden can't fail, because the server skips hidden fields. If a required field keeps failing,
check that your own UI sends it under the exact name, with groups as nested objects.

### Rejected As Spam

The response doesn't say which check caught it, so go through all three under **Settings**, in **Anti-spam**:

| Check | What trips it | Fix |
| --- | --- | --- |
| Hidden field | The hidden field has a value. New forms use `_formerie_hp` | In your own UI, leave that field out or empty |
| **Minimum submit time** | `metadata.startedAt` is more recent than the limit, 1 second on new forms | Set `startedAt` when the form first appears, not on submit, or lower the limit |
| **Maximum links** | More links than the limit across all answers, 3 on new forms. Web addresses in `url` fields count too | Raise the limit. `0` rejects any link, and an empty setting turns the check off |

### Too Many Submissions

`RATE_LIMITED` comes from one of two places:

- **Submission limits**, under **Settings**. New forms allow 10 submissions per visitor every 10 minutes, and 300 for the whole
  form every hour. Raise them if your form gets real bursts, up to 1,000 an hour for a form on the Free plan. People on the same network, such as an office, share the
  per-visitor limit.
- A short-term limit on public routes, which you can't change. It sends `Retry-After`, in seconds.

Tell the visitor to wait, and keep their answers on screen.

### Repeat Sends

`DUPLICATE_SUBMISSION` means the first send already worked, and that's good news. Show your success message. Send a new
`metadata.idempotencyKey` for each new submission, and reuse a key only to retry the same one.

## The Form Isn't Showing

**Symptom:** the link shows an error, or the page where you embedded the form is blank.

**How to check:** open the form's **Hosted URL** in a private window. If it works there, the form is fine and the
problem is in the page around it. If you see a line of JSON, read its `code`.

| What you see | Cause | Fix |
| --- | --- | --- |
| `FORM_NOT_FOUND` | The form was never published, **Public access** is on **Unpublished**, or the address is wrong | Publish on the **Publish** tab, and copy the address from there |
| `FORM_CLOSED`, `FORM_NOT_OPEN` | The form is closed, or outside its **Response window** | Change them on the **Publish** tab |
| `FORM_MAINTENANCE` | **Public access** is on **Maintenance** | Set it back to **Public** |
| `FORM_NOT_FOUND` on your own hostname | The hostname isn't active or isn't connected to this form | Finish the DNS steps under **Domains**, and check its connection |
| "The form could not be loaded." or "Loading form..." | The browser couldn't load the form's script | Check for a blocked request to `cdn.formerie.com` in the browser's network panel |
| An old version of the form | Your changes are still a draft | Choose **Publish changes** |
| A question is missing | A condition is hiding it, or it's on a later step | Review **Conditions** and **Flow** |

### An Embed Shows Nothing

- The script adds its iframe right where the tag sits. With `data-formerie-target`, the selector has to match something on the
  page when the script runs.
- `data-formerie-form` must be `workspace-handle/form-handle`, as in the **Embed code**. A wrong value shows `FORM_NOT_FOUND` inside the iframe.
- If your site sets a Content Security Policy, allow `https://forms.formerie.com` as a frame and script source, and
  `https://cdn.formerie.com` as a script source.

### A Browser Mount Shows Nothing

The browser mount loads the form with a call from your page. Without your site's address in the allowed origins, that call fails.
See the next section.

## A CORS Or Origin Error

**Symptom:** your browser's console shows `blocked by CORS policy`, your code gets a network error such as `Failed to fetch` or the SDK's `NETWORK_ERROR`,
and the request shows as 403 or 404 in the network panel. Your code doesn't get to read a Formerie code, because a refused browser
request comes back without the headers that would let it.

**How to check:** this only happens when browser code on your own site calls Formerie, which means the browser mount, a
framework package used in the browser, or your own `fetch`. A hosted link and a script embed don't call from your site, so they
can't cause it.

Open the failed request in the network panel. It's often the `OPTIONS` one, and its response says `FORBIDDEN_ORIGIN` or
`FORM_NOT_FOUND`. Compare the request's `Origin` header with your list.

| Cause | Fix |
| --- | --- |
| Your site isn't listed | Add it under **Your website addresses** on the **Publish** tab, or **Allowed origins** in **Settings**, then publish |
| You added it but didn't publish | Origins take effect when you publish. **Publish** saves them for you |
| The address differs slightly | Matches are exact. `https://example.com`, `https://www.example.com`, and `http://example.com` are three entries, so add each one your site answers on |
| You're testing locally | Add your dev address with its port, such as `http://localhost:5173` |
| You used a path or a wildcard | Use the address only. Paths fail with `FORM_CONFIG_ORIGIN_INVALID`, and `*` with `FORM_CONFIG_ORIGIN_WILDCARD` |
| The response says `FORM_NOT_FOUND` | The workspace or form handle is wrong, or the form isn't published. It looks like a CORS error because no form means no list of origins to allow |
| You added your own request header | Browsers may only send `Content-Type` here. Remove the custom header, or send it from your server |

Code on your own server doesn't need any of this, unless it forwards an `Origin` header. Without one, the check doesn't apply. See
[Allowed Origins](/guides/spam-protection/#allowed-origins).

## Verification Failed

**Symptom:** a submission returns `TURNSTILE_FAILED`. It's the same code for Cloudflare Turnstile and for reCAPTCHA, and the
response doesn't give a reason.

**How to check:** look at the form's **Human verification** settings under **Settings**, then go through these causes.

| Cause | Fix |
| --- | --- |
| No token was sent | Your own UI needs to pass the provider's token as `verificationToken`. Hosted pages, embeds, and the browser mount handle this for you |
| The token was already used, or expired | Tokens work once. Get a new one for each attempt, including after a failed submit |
| The widget's domain doesn't match | Formerie checks that the token came from the site making the request. See the next section |
| **Formerie managed** on your own site | Managed keys work on supported Formerie-hosted domains. Use your own keys for your own domain |
| Wrong keys | The site key and secret key must come from the same widget. Save them again under **Human verification** |
| **Expected action** doesn't match | It must match the action your widget sends |
| reCAPTCHA v3 score too low | Lower **Minimum score**, from 0 to 1 |

### Which Domain To Add To Your Widget

With your own keys, the provider's console lists the domains your widget works on. Add the domain the widget loads on:

| How you publish | Domain to add |
| --- | --- |
| Hosted link | `forms.formerie.com`, or your own connected hostname |
| Script embed | `forms.formerie.com`, or your own connected hostname, because the widget runs inside the iframe |
| Browser mount or your own UI | Your own site |

### Verification Through Your Own Server

A token from your page, sent on through your own server, can fail on the domain check. Without an `Origin` header, Formerie
compares the token's domain with its own. To pass, forward the visitor's `Origin` header with the request, and list that site
under allowed origins too.
