Contact Forms and Newsletter Signups
Add a contact form or a newsletter signup to any website with one script tag, or call the Forms API from your own design. Messages land in your dashboard and your inbox, with spam protection and clear limits.
New to Forms? See the Forms overview and plans, or add an AI chat widget to the same site.
What Forms is
Forms is a separate product with its own plans: you do not need an API plan, credits or a chatbot. A contact form collects messages from your visitors into an inbox you can search, answer and export. A newsletter signup collects email addresses with double opt-in (the person confirms by email). We collect and store them; you send your newsletters with your own email tool.
Plans and limits
| Plan | Price / month | Forms | Messages / month | Stored subscribers | Extras |
|---|---|---|---|---|---|
| Forms Free | Free | 1 | 50 | 100 | Basic spam protection |
| Forms Starter |
|
5 | 1,000 | 1,000 | AI spam check (500/mo) |
| Forms Pro |
|
25 | 10,000 | 10,000 | AI spam check (5,000/mo) , AI tags (5,000/mo) , AI reply drafts (300/mo) , auto-reply , webhook , no “Powered by” line |
Already on a paid API plan? Forms plans are 15% cheaper, applied automatically at checkout. The free plan needs no payment and no phone verification. Spam is never counted against your limit. Manage your plan from Dashboard → Billing.
Install in 4 steps
Create a form
Open Dashboard → Forms and choose Contact form or Newsletter signup.
Set it up in the Settings tab
Choose the fields (up to 8 for a contact form), the button and thank-you texts, an optional redirect, colours, a privacy checkbox, and where email alerts go.
Copy the code from the Install tab
It is one line and never changes, even when you change the settings later:
<script src="https://rsflowhub.com/widget/v1/rsflowhub-forms.js"
data-form-token="fm_sec_YOUR_FORM_TOKEN"
async></script>
Paste it where the form should appear
The form is drawn right where the tag is. Pick your platform:
<section>
<h2>Contact us</h2>
<script src="https://rsflowhub.com/widget/v1/rsflowhub-forms.js"
data-form-token="fm_sec_YOUR_FORM_TOKEN"
async></script>
</section>
<!-- Add a "Custom HTML" block where the form should appear, then paste: -->
<script src="https://rsflowhub.com/widget/v1/rsflowhub-forms.js"
data-form-token="fm_sec_YOUR_FORM_TOKEN"
async></script>
{% comment %} Online Store > Themes > Customize > Add section > Custom Liquid {% endcomment %}
<script src="https://rsflowhub.com/widget/v1/rsflowhub-forms.js"
data-form-token="fm_sec_YOUR_FORM_TOKEN"
async></script>
import { useEffect, useRef } from 'react';
export default function ContactForm() {
const box = useRef(null);
useEffect(() => {
const s = document.createElement('script');
s.src = 'https://rsflowhub.com/widget/v1/rsflowhub-forms.js';
s.async = true;
s.setAttribute('data-form-token', 'fm_sec_YOUR_FORM_TOKEN');
box.current.appendChild(s); // the form is drawn right after the script tag
return () => { box.current.innerHTML = ''; };
}, []);
return <div ref={box} />;
}
{{-- Anywhere in a Blade view --}}
<script src="https://rsflowhub.com/widget/v1/rsflowhub-forms.js"
data-form-token="fm_sec_YOUR_FORM_TOKEN"
async></script>
To place it inside a specific element, add data-target:
<div id="contact-box"></div>
<script src="https://rsflowhub.com/widget/v1/rsflowhub-forms.js"
data-form-token="fm_sec_YOUR_FORM_TOKEN"
data-target="#contact-box"
async></script>
The
fm_sec_… token only lets visitors send messages to that one form and is meant to appear in your page source. To limit where it works, list your websites under Allowed websites in the form settings.
What the embed does
- It lives in its own isolated box: your site's CSS cannot break it and its CSS does not touch your site. It works under a strict Content-Security-Policy (no inline styles or scripts).
- It is accessible: every field has a label, errors are announced and focus moves to the first problem, and inputs are large enough that phones do not zoom.
- It shows quick checks (required, valid email) while the server does the real validation.
- When the message is sent it shows your thank-you text (or redirects to your
httpspage) and fires a browser event you can use for analytics:document.addEventListener('rsflowhub:form-submitted', e => console.log(e.detail))withdetail.type(contactornewsletter) anddetail.ref. - Free plans show a small “Powered by RSFlowHub” line; Forms Pro removes it.
Use the API from your own design
Looking for every endpoint, body field and answer? See the full Forms API Reference.
Build the form in your own HTML and send the fields as JSON. The token goes in the X-Form-Token header. Fields are the ones you defined in the settings, keyed by their Field ID; unknown keys are ignored. Requests from browsers must come from one of your allowed websites (if you listed any).
Send a contact message
POST https://rsflowhub.com/api/v1/forms/submissions
curl -X POST https://rsflowhub.com/api/v1/forms/submissions \
-H "X-Form-Token: fm_sec_YOUR_FORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fields": {"name": "Ann", "email": "ann@example.com", "message": "Hello!"}, "consent": true}'
200: saved
{
"status": "success",
"message": "Thanks! Your message has been sent.",
"data": { "ref": "FRM-AB12CD" }
}
422: a field is invalid
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"details": { "fields.email": ["The fields.email field must be a valid email address."] }
}
}
| Body field | Meaning |
|---|---|
fields | Object with one value per field, keyed by Field ID (name, email, message, ...). |
consent | true when the visitor accepted your privacy notice. Required when you turned on Ask visitors to accept a privacy notice; the time is stored with the message. |
website | Leave this out (or empty). It is a hidden trap for bots: if it is filled the request is accepted but nothing is stored. Do not render it in your own form, or render it hidden. |
Add a newsletter subscriber
POST https://rsflowhub.com/api/v1/forms/subscribers (newsletter forms only; the contact endpoint above is for contact forms only).
curl -X POST https://rsflowhub.com/api/v1/forms/subscribers \
-H "X-Form-Token: fm_sec_YOUR_FORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fields": {"email": "reader@example.com"}, "consent": true}'
The person is stored as waiting and receives an email with a confirmation link (valid 7 days). Only after they click it do they count as a subscriber. The answer is the same for every address, new or not, so your form cannot be used to find out who is on your list.
Status codes
| Code | Meaning |
|---|---|
200 | Accepted. Spam is accepted too (visitors and bots see the same answer); it is kept in your Spam filter. |
401 | Missing, wrong or paused form token. |
403 | The request does not come from one of your allowed websites. |
404 | Wrong endpoint for this form type. |
422 | A field is missing or invalid. error.details maps fields.<id> to messages. |
429 | Too many requests from one visitor, or your plan's monthly limit is reached (code is monthly_limit or subscriber_limit). The text is safe to show to visitors. |
Your inbox and exports
- Messages: filter Inbox / New / Read / Replied / Spam, search, open a message (it is marked read), reply by email, mark it replied, mark or rescue spam, delete, or export everything as CSV. You also get an email for each new message (at most 10 per hour per form; everything stays in the dashboard).
- Subscribers: Waiting, Confirmed and Unsubscribed with search and CSV export. The CSV contains each person's permanent unsubscribe link: put it in every email you send from your own tool.
- Spreadsheet-safe: exported cells that start with
=,+,-or@are neutralised so a visitor cannot run a formula on your computer.
Spam protection
- On every plan: a hidden trap field for bots, a limit on how fast one visitor can send, and rules that look for many links, throwaway email domains, repeated text and common spam phrases. Messages that look like spam are kept for review (nothing is silently lost) and do not count towards your limit.
- Starter and Pro: an optional AI second opinion on messages the rules did not flag. If the AI is unavailable or your monthly AI allowance is used up, your real messages still arrive; the helper simply pauses.
AI helpers, auto-reply and webhook
- AI tags (Pro): each message gets a label such as sales, support or billing and a mood (positive, negative, neutral, mixed) shown in your inbox.
- Suggested reply (Pro): a draft answer you review and send yourself. It is never sent automatically. Only the visitor's words are sent to the AI, not their email address.
- Automatic reply (Pro): an optional “we received your message” email with your own words. One per address per day, and never containing what the visitor typed.
- Webhook (Pro): every new message (and every confirmed subscriber) is sent to an https address you choose as a signed JSON request. Addresses on private networks are refused, redirects are not followed, and failures are retried a few times. Use Send a test event in the settings to try it.
{
"event": "form.submission.created",
"form": { "name": "Website contact form", "type": "contact" },
"data": {
"ref": "FRM-AB12CD",
"received_at": "2026-10-12T09:30:00+00:00",
"fields": { "name": "Ann", "email": "ann@example.com", "message": "Hello!" },
"consented_at": null
}
}
Each request carries X-RSFlowHub-Event (form.submission.created, form.subscriber.confirmed or form.test) and X-RSFlowHub-Signature: sha256=…, the HMAC-SHA256 of the raw body with the secret shown in your form settings. Always verify it and answer with a 2xx status:
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $yourWebhookSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_RSFLOWHUB_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
// the request really came from RSFlowHub: $event = json_decode($body, true);
import crypto from 'node:crypto';
// Use the RAW request body (express.raw), not a re-serialised object.
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(rawBody).digest('hex');
const received = req.get('X-RSFlowHub-Signature') || '';
const ok = received.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
Privacy and data
- Consent: turn on the privacy checkbox and a message or signup cannot be sent without it. The time they accepted is stored.
- Retention: choose how long messages are kept (30, 90, 180 or 365 days, or until you delete them). Older messages, and subscribers who unsubscribed longer ago than that, are deleted every night. Signups that were never confirmed are removed after 30 days whatever you choose; confirmed subscribers are never deleted automatically.
- Access and erasure requests: on the Forms page, enter the person's email under Privacy requests, then Download their data (a JSON file) or Delete their data (messages and newsletter signups in all your forms).
- Visitor IP addresses are never stored, only a keyed hash used to spot abuse. Text sent to your inbox is cleaned of HTML.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| Nothing appears on my page | Open the browser console: “the form token or website is not allowed” means a wrong or regenerated token, a paused form, or your site is not in Allowed websites. An ad blocker or a Content-Security-Policy that blocks rsflowhub.com also hides it. |
403 Origin not allowed | Add the site (and its www or subdomain variant) to Allowed websites, or clear the list to allow any site. Server-side or curl requests carry no Origin and are refused while a list is set; call the API from the browser, or clear the list. |
| Visitors see “not accepting messages right now” | Your monthly message limit (or stored-subscriber limit) is used up. Upgrade from Billing; you were emailed at 80% and 100%. |
| No email alerts | Check Email alerts in Settings, the address, and your spam folder. At most 10 alerts per hour are sent per form; every message is still in the dashboard. |
| A real message went to Spam | Open the Spam filter, open the message and choose Not spam. It then counts towards your limit and is treated as a normal message. |
| My changes don't show | Settings load on each page view. Hard-refresh, and clear any page cache or CDN in front of your site (the script and settings are fetched separately). |