Add data-aeo-form to a normal HTML form to send its fields to Answer Cloud.
The submission creates a new conversation in the existing chat inbox, saves the
submitted fields, and queues a notification for the organization's team. The AI responds using the same site content and chat
experience as the chat widget. Visitors can continue the conversation there.
No API key or separate form service is required. The public organization and
site IDs identify the destination; they are not secrets.
Add a form to a hosted site
Answer Cloud injects embed.js and the site's settings when publishing a hosted
site. Add the following markup to an Astro page or component. Do not add another
embed script, AI SDK, or local form endpoint.
<form data-aeo-form="Contact" method="post">
<div>
<label for="contact-name">Name</label>
<input id="contact-name" name="name" autocomplete="name" required maxlength="100">
</div>
<div>
<label for="contact-email">Email</label>
<input id="contact-email" name="email" type="email" autocomplete="email"
required maxlength="254">
</div>
<div>
<label for="contact-phone">Phone (optional)</label>
<input id="contact-phone" name="phone" type="tel" autocomplete="tel" maxlength="100">
</div>
<fieldset>
<legend>I'm interested in (optional)</legend>
<label><input type="checkbox" name="interests" value="Consulting"> Consulting</label>
<label><input type="checkbox" name="interests" value="Support"> Support</label>
</fieldset>
<div>
<label for="contact-message">How can we help?</label>
<textarea id="contact-message" name="message" rows="5" required
maxlength="5000"></textarea>
</div>
<button type="submit">Send message</button>
<p data-aeo-form-status role="status" aria-live="polite"></p>
</form>
<noscript>Please enable JavaScript to send this form.</noscript>
Apply the site's existing form styles. Keep visible labels, a visible keyboard
focus indicator, and readable pending, success, and error text. Use unique IDs
if the page contains more than one form.
data-aeo-form="Contact" names the submission. Use a descriptive name such as
Request a quote or Book a demo. Every field needs a name; use conventional
name, first_name, last_name, email, phone, and company names for contact
details. Other named fields are saved in the conversation too.
Add a form to an external website
Set window.aeoSettings before loading the embed script. Replace 123 and 456
with the organization and site IDs for the intended site. Keep an existing
embed installation and add only the form when the script is already present.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Contact us</title>
</head>
<body>
<main>
<h1>Contact us</h1>
<form data-aeo-form="Contact" method="post">
<div>
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required maxlength="100">
</div>
<div>
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email"
required maxlength="254">
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required maxlength="5000"></textarea>
</div>
<button type="submit">Send message</button>
<p data-aeo-form-status role="status" aria-live="polite"></p>
</form>
<noscript>Please enable JavaScript to send this form.</noscript>
</main>
<script>
window.aeoSettings = {
organization_id: 123,
site_id: 456,
chat: { enabled: true }
};
</script>
<script src="https://app.answer.cloud/embed.js" defer></script>
</body>
</html>
Forms also work when chat.enabled is false: the visitor receives inline
status and the team still receives the submission. On a hosted site, use the
platform's generated settings rather than replacing window.aeoSettings.
Submission behavior
- The browser applies native constraints such as
required,type="email",
and maxlength before a normal submission. These do not replace server-side
business validation in a custom integration.
- The handler sends named text inputs, textareas, selects, and checked choices.
Repeated names and multiple selected values become arrays. Disabled,
password, file, hidden, unchecked, unnamed, and button controls are excluded.
- Submit controls are disabled while the request is pending. A second submit
during that request does not create another request. Controls are restored
afterward.
- The status element shows progress, success, or an error. If no descendant
with data-aeo-form-status exists, the handler creates an accessible status
element.
- Field values stay in the form after both success and failure. A later
deliberate submission creates another conversation.
- Each accepted form starts a new chat conversation. With chat enabled, the
widget opens that conversation and subsequent messages use the normal chat
endpoint. This replaces the widget's current conversation history for the
site.
- Add
data-aeo-open-chat="false"to the form to keep the visitor on the page
with inline status instead of opening the widget automatically.
The server saves contact fields directly; notification delivery does not depend
on the AI recognizing an email address. If AI generation fails, the saved
submission receives a fallback acknowledgment and the team notification is
still queued. A successful response confirms capture, not email delivery or a
promised response time from the team.
This integration requires JavaScript and a network connection. File uploads and
offline or native HTML POST fallbacks are not supported. Use it for contact and
inquiry forms; do not collect passwords, payment details, or other secrets.
The handler does not retry automatically. A network interruption or server
error can occur after the server has saved a submission, so check the chat
inbox before resubmitting when avoiding a duplicate matters.
Customize the page after submission
Listen on the form for aeo:form-success or aeo:form-error. Success event
detail contains the parsed API response; error event detail.error contains
the error object. These events let a site update its own UI alongside the
provided status and chat behavior.
<script>
const form = document.querySelector('[data-aeo-form="Contact"]');
form.addEventListener("aeo:form-success", (event) => {
console.log("Saved conversation:", event.detail.conversation.id);
});
form.addEventListener("aeo:form-error", (event) => {
console.warn(event.detail.error.message);
});
</script>
Call the API from a custom form
POST https://app.answer.cloud/api/form_submissions accepts JSON without an
authorization header. Always send the intended organization_id; include
site_id and the current site_origin for a hosted site so the conversation
and AI retrieval stay scoped to it.
const response = await fetch("https://app.answer.cloud/api/form_submissions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
organization_id: 123,
site_id: 456,
site_origin: window.location.origin,
form: {
name: "Contact",
fields: {
name: "Jane Example",
email: "jane@example.com",
message: "Can you help with our project?",
interests: ["Consulting", "Support"]
}
}
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Unable to send the form. Please try again.");
// data.conversation, data.user_message, data.ai_message, data.references
Optional session_id and tracking use the same payload as /api/messages.
When the embed is available, obtain them with
window.AEOTracking.buildPayload(): send its session_id and use its
custom_data as tracking. The embed-managed form handler already does this.
The successful response is 201 Created and has the same shape as a chat
message response:
{
"conversation": { "id": 789, "organization_id": 123, "site_id": 456 },
"user_message": { "id": 1001, "role": "user", "content": "Form submission: Contact..." },
"ai_message": { "id": 1002, "role": "assistant", "content": "Thanks for reaching out..." },
"references": []
}
The objects include additional chat attributes. Save conversation.id and pass
it as conversation_id to /api/messages to continue that conversation. The
custom API call does not itself open or update the widget; the embed's form
handler provides that integration.
The API accepts at most 50 fields. Form and field names must be single-line text
without null characters and are limited to 100 characters, each value to 5,000 characters, and the combined form name, field
names, and values to 20,000 characters. Each field can contain text, a number,
a boolean, null, or an array of at most 50 text values. Scalars are converted
to trimmed text; null becomes empty text. Nested objects and submissions with
no nonempty value are rejected. The form name defaults to Contact form when
omitted or blank.
Handle non-success responses as errors and preserve the visitor's input. An
unknown organization or an out-of-scope conversation returns 404; managed
hosted sites also reject unknown site IDs or mismatched origins with 404.
Invalid submissions return 422 Unprocessable Entity with JSON such as
{"error":"form must contain a nonempty value"}.
Accepted forms record a server-side form_submit conversion with the
conversation and form identifiers. Field values are not included in the
analytics event. The embed's generic form tracking skips these forms to avoid
counting the same submission twice.
Verify the implementation
Use a test organization or coordinate any real submission with the site owner:
successful tests queue team notifications.
- Check labels, keyboard navigation, narrow screens, and native required/email
validation.
2. Submit a short test inquiry. Confirm the inline success state, retained
values, and the intended chat opening behavior.
3. Confirm the new conversation appears in the correct site's chat inbox with
all submitted fields and contact details. Send a chat follow-up and confirm
it stays in that conversation.
4. Confirm a single conversion and, separately, notification delivery.
5. Simulate a failed request locally; confirm the error is announced, controls
recover, and field values remain available for retry.
For hosted sites, verify the deployed page: a local Astro preview does not
include the generated embed integration.