# Handling failed submissions and offline users

> Keep the visitor's text on screen whenever a send fails. Retry network errors and 429 responses with a short backoff, and fix the request for 400, 403, 404 and 413, since retrying those will fail again. Test each path with the network turned off and with a bad key.

Source: https://escutaproduto.com/resources/feedback-submit-errors-offline
Last updated: 2026-10-11

## What do the feedback API error codes mean?

The REST API returns a status code for every failed request, and each one points to a different fix. A `400` means the body is invalid, most often because the key or the message is missing or shorter than two characters. A `403` means the origin of the request is not on the product's allowed list. A `404` means the key does not match any product. A `413` means the body is larger than 16 KB. A `429` means the same product received more than 10 requests per minute from the same IP address.

A successful submission returns `201` with a JSON body that holds `ok: true` and the new feedback `id`. Treat anything else as a failure and show the visitor a message that matches the cause.

## Which failures should you retry and which should you fix?

Retrying a request that will fail again only wastes the visitor's time and adds load to the server. Sort the codes into two groups:

| Code | Meaning | Action | Retry? |
| --- | --- | --- | --- |
| 400 | The body is invalid or a required field is missing | Check the key, the message length and the field types | No, fix the request |
| 403 | The site origin is not allowed | Add the site to the product's allowed origins | No, fix the settings |
| 404 | The public key is unknown | Check the key, and update it after a key rotation | No, fix the key |
| 413 | The body is over 16 KB | Shorten the message or drop large metadata | No, shorten it |
| 429 | Too many requests from one IP for this product | Wait, then send again with a delay | Yes, with backoff |
| Network error | The request never reached the server | Keep the text and offer to try again | Yes, when the connection returns |

The rate limit is per IP and per product, so a shared office network can hit it faster than a single person would. A short delay usually clears it.

## How do you keep the message when a send fails?

The worst outcome is a failed send that clears the text. Keep the message in state until the server confirms success, and only clear it after a `201` response. If the send fails, leave the text in place, show a clear message next to the send button and move focus to that message so a screen reader user hears it.

```js
async function submitFeedback(payload) {
  const response = await fetch("https://escutaproduto.com/api/v1/feedback", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (response.status === 201) return { ok: true };
  return { ok: false, status: response.status };
}
```

The function returns the status instead of throwing, so the form can choose the message for each code. A network failure makes `fetch` reject, so wrap the call in `try` and `catch` and treat a rejection as a network error.

## How do you back off after a 429 response?

Retry a 429 a few times with growing delays, then stop and tell the visitor to try again later. Limit the retries, so a broken form does not loop forever:

```js
async function sendWithBackoff(payload, attempt = 0) {
  let result;
  try {
    result = await submitFeedback(payload);
  } catch (error) {
    result = { ok: false, status: 0 };
  }
  if (result.ok) return result;
  const retryable = result.status === 0 || result.status === 429;
  if (!retryable || attempt >= 3) return result;
  await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt));
  return sendWithBackoff(payload, attempt + 1);
}
```

The delays are one, two and four seconds. Show the visitor a status line while the retries run, so the form does not look frozen.

## How do you handle a visitor who is offline?

Offline visitors are a different case. Do not guess the connection state from `navigator.onLine` alone, because the browser reports a connection even when the server cannot be reached. Instead, rely on the failed request itself, and use the `online` event to retry a message that is waiting.

For your own form, you can store an unsent message in `localStorage` and send it when the connection returns. Wrap every read and write in `try` and `catch`, because storage can be full or blocked in private windows. Remove the stored draft only after the server returns `201`. Keep the saved text private to the visitor's browser, and never store anything you would not want on a shared computer.

## What does the widget do on failure, and how do you test it?

Do not assume the widget behaves like your own form. Test it. Open the widget, type a message, turn off the network in DevTools or switch off Wi-Fi, then send. Check whether the text is still in the panel when the network returns, and whether the visitor can send it again without retyping. Then repeat the test with a page that is not in the allowed origins list, which produces a `403`. Write down what you see, and adjust the instructions on your site. If the text is lost on a network failure, tell visitors to copy a long message before they send.

Run these checks before you release a form change:

1. Send a valid message and confirm the `201` path clears the form.
2. Turn off the network, send, and confirm the text stays.
3. Send from an origin that is not allowed and confirm the `403` message makes sense to a visitor.
4. Send a message of 17 KB and confirm the `413` message appears.
5. Send several messages in a row and confirm the `429` path shows a wait message.

## Recovering failed sends in an Escuta Produto integration

Use the widget for the common path and your own form only when you need custom error text or a different layout. The request format, the allowed origins and the key rules are in the [API docs](/docs/api). The steps for adding the widget to a page are in the [widget docs](/docs/widget). For the other side of a failed send, when a message does arrive, [how to triage customer feedback](/resources/how-to-triage-customer-feedback) explains how to sort it. For spam protection that can cause a request to be rejected, [how to protect a feedback form from spam](/resources/feedback-form-spam-protection) covers the origin and rate rules in more depth.

## Frequently asked questions

### Should a feedback form retry every failed submission?

No. Retry network errors and 429 responses, with a short delay and a limit on attempts. Fix the request for 400, 403, 404 and 413, because sending the same body again will fail the same way.

### What should happen to the visitor's message when a send fails?

The text should stay in the form. Clear it only after the server confirms success with a 201 response, and show a message that says what went wrong and what to do next.

### What does a 429 error mean for a feedback form?

The same IP address sent too many requests to the same product in a short period, which is more than 10 per minute. Wait a few seconds, then send again, and do not loop retries without a limit.
