Public forms
Accept a website form without putting mail credentials or recipient routing in browser code.
Configure the form
Choose which websites can submit the form and where submission emails are delivered.
An internal name used to identify this form in Console.
The address shown as the sender. It does not need a mailbox or alias, but its domain must belong to this organisation and be enabled and verified for sending.
Websites permitted to submit this form. Enter one origin per line, including the scheme and optional port, for example https://example.com or http://localhost:3000.
These are field-name overrides. Keep the preset values when using the generated HTML + JavaScript snippet. Change them only when connecting an existing form that uses different field names.
Console also shows the deployment-specific endpoint and public form ID. The public ID is not a secret; API tokens, mailbox passwords, recipients, and sender routing must never be placed in browser code.
Use the widget
The versioned Web Component is the recommended integration. It loads the configured fields, CAPTCHA, validation, pending state, success view, and retry behaviour without a framework dependency.
Vanilla JavaScript
<emaia-form form-id="YOUR_FORM_ID"></emaia-form>
<script type="module" src="https://emaia.eu/form-widget/v1.0.0/widget.js"
integrity="sha384-6g38ahC+3IGToFINIG+kiy2GNPhNkTwaLHpzEu7b9XUMTIA5Io5or8Bbz4GpTXYP" crossorigin="anonymous"></script>React
// public/index.html
<script type="module" src="https://emaia.eu/form-widget/v1.0.0/widget.js"
integrity="sha384-6g38ahC+3IGToFINIG+kiy2GNPhNkTwaLHpzEu7b9XUMTIA5Io5or8Bbz4GpTXYP" crossorigin="anonymous"></script>
// src/custom-elements.d.ts
import type { HTMLAttributes } from "react";
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"emaia-form": HTMLAttributes<HTMLElement> & { "form-id": string };
}
}
}
// src/ContactForm.tsx
export function ContactForm() {
return <emaia-form form-id="YOUR_FORM_ID" />;
}Angular
// src/index.html
<script type="module" src="https://emaia.eu/form-widget/v1.0.0/widget.js"
integrity="sha384-6g38ahC+3IGToFINIG+kiy2GNPhNkTwaLHpzEu7b9XUMTIA5Io5or8Bbz4GpTXYP" crossorigin="anonymous"></script>
// src/app/app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
@Component({
selector: "app-contact",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: '<emaia-form form-id="YOUR_FORM_ID"></emaia-form>',
})
export class AppComponent {}Framework integrations use the same custom element. The widget emits these lifecycle events:
emaia-form-ready, emaia-form-accepted,emaia-form-error
Widget versioning
Pin an exact version. Exact-version files are immutable and behaviour changes ship at a new URL, so an existing website cannot change unexpectedly. Review release notes and test before changing the version. The challenge contract is negotiated separately so future CAPTCHA systems do not silently break pinned widgets.
Get a CAPTCHA
GET /api/v1/forms/{form_id}/captcha
Call this without credentials from an allowed origin. Render the returned arithmetic question and keep its ID only long enough to submit the answer. Load a new challenge after each accepted or rejected submission.
Submit fields
POST /api/v1/forms/{form_id}/submissions
Send JSON containing captchaId, numeric captchaAnswer, and a fields object whose names match the Console configuration. A 202 Accepted response contains a message ID and means the submission was queued, not finally delivered.
Browser example
const base = window.PUBLIC_FORM_API_BASE;
const formId = window.PUBLIC_FORM_ID;
const captchaResponse = await fetch(
`${base}/api/v1/forms/${encodeURIComponent(formId)}/captcha?challenge=arithmetic-v1`,
{ credentials: "omit" },
);
if (!captchaResponse.ok) throw new Error("CAPTCHA unavailable");
const captcha = await captchaResponse.json();
if (captcha.type !== "arithmetic-v1") throw new Error("Unsupported CAPTCHA");
const captchaAnswer = Number(document.querySelector("#captcha-answer").value);
const response = await fetch(
`${base}/api/v1/forms/${encodeURIComponent(formId)}/submissions`,
{
method: "POST",
credentials: "omit",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
captchaId: captcha.id,
captchaAnswer,
fields: {
name: "Ada Example",
email: "ada@example.net",
subject: "Hello",
message: "Please contact me.",
},
}),
},
);
if (response.status !== 202) throw new Error("Submission not accepted");
const accepted = await response.json();
console.log(accepted.messageId);Replace the placeholders with the endpoint and public form ID shown in Console. Provide accessible labels, disable duplicate submission while a request is pending, and show a clear success or failure state to the visitor.
A single-choice topic menu can replace the generated subject input. Keep name="subject", or set the Subject field override in Console when an existing form uses another name such as topic. The selected value becomes the email subject.
<label>
Topic
<select name="subject" required>
<option value="">Choose a topic</option>
<option value="Sales">Sales</option>
<option value="Support">Support</option>
</select>
</label>A public form has one fixed recipient. Use separate public forms when different topics must be delivered to different addresses.
Browser security
- No API token or mailbox credential is accepted or needed.
- The request origin must exactly match an enabled allowed origin.
- Recipient, sender, carbon-copy, headers, attachments, templates, and other mail-routing fields supplied by the browser are rejected.
- Treat every submitted field as untrusted input in downstream workflows.
Errors and retries
400: correct malformed fields or CAPTCHA data.401: load a new CAPTCHA and ask the visitor to try again.403: verify that the form is enabled and the exact origin is allowed.413: reduce the request body.429: pause and followRetry-Afterwhen present.500or a network failure: show a retry option; do not promise that an uncertain submission was not accepted.



