There are two ways to let people subscribe, and which one you want depends on whether you have a backend.
| Signup form (widget) | Server-side API | |
|---|---|---|
| Where it runs | Any website, front-end only | Your backend |
| Credential | A public form id, safe in page source | A secret API key (ls_…) |
| Setup | Paste one <script> tag |
Write a route handler |
| Opt-in | Always double opt-in | Follows your workspace setting |
Start with the signup form. The API is for when you already have a backend doing something else with the address.
Option 1 — the signup form widget
Create one under Newsletter → Signup forms, then paste the snippet:
<script src="https://api.vlozi.app/newsletter/f/embed.js" data-newsletter="nlf_…"></script>That is the whole integration. The widget renders an email field (and a name field, if you enabled one), posts the signup, and shows your success message. No API key, no backend, no CORS configuration.
Why the form id is safe to publish
The id in that snippet is a capability, not your workspace identity:
- It is random and unguessable, so nobody finds it by scanning.
- It does exactly one thing — add an address to that one list. It cannot read your subscribers, cannot tell an attacker whether an address is already on the list, and cannot unsubscribe anyone.
- It is revocable. Pause the form and the id stops working within 30 seconds, with your subscriber list untouched. Then mint a new one.
Your workspace id is not a secret and never appears here. Nothing about the request tells Vlozi which workspace to write to — the form id does, server-side.
Placing it precisely
The one-liner drops the form wherever the <script> tag sits. To control the
position, put a container anywhere on the page and the script anywhere else:
<div data-vlozi-newsletter="nlf_…"></div>
<script src="https://api.vlozi.app/newsletter/f/embed.js"></script>One script tag can render several containers on the same page.
Styling
Set Accent colour and Button label on the form. Everything the widget
renders is prefixed .vlzn, so you can restyle it from your own stylesheet:
.vlzn-i { border-radius: 8px; } /* inputs */
.vlzn-b { border-radius: 8px; } /* button */
.vlzn-msg { font-style: italic; } /* success / error message */Settings that matter
| Setting | What it does |
|---|---|
| Tags | Applied to every signup from this form. Set server-side, so a visitor cannot choose their own tags — build a segment on the tag to target this audience. |
| Allowed origins | Restricts which websites may submit from a browser. Empty means any. Once you list one, requests with no Origin header are rejected too. |
| Redirect after signup | Sends the visitor to your own thank-you page instead of showing a message. |
| Consent line | Small print under the form. Say what you'll send and how often. |
| Turnstile captcha | Off by default. The honeypot and email confirmation already carry most of the load, and a challenge on a two-field email box costs signups. Needs a public site key. |
NOTE
Allowed origins is not a password. It constrains browsers on other people's sites; it does not stop a script posting directly. That is expected — any public signup form accepts submissions from anywhere, which is why the confirmation email exists. Use it to reduce noise, not as your only defence.
What stops abuse
Because this endpoint is anonymous, it is bounded on several axes at once:
- Every widget signup requires email confirmation, regardless of your workspace's opt-in setting. Someone submitting a stranger's address cannot put them on your list — only that person, from their own inbox, can.
- A hidden honeypot field silently discards obvious bots. They get a normal success response and nothing is written.
- Rate limits apply per visitor per form, and per visitor per workspace.
- A per-address cooldown means repeatedly submitting the same email sends one confirmation, not many. Your form cannot be turned into a mail bomb aimed at someone else.
Option 2 — subscribing from your backend
Use this when a signup is part of something bigger — a checkout, an account creation, a CRM sync.
POST https://api.vlozi.app/newsletter/public/subscribeHeaders:
Content-Type: application/json
x-api-key: ls_your_secret_keyCreate the key under Settings → API Keys, choose Secret, and grant it
newsletter:subscribers.create.
IMPORTANT
This key must stay on your server. Publishable keys (pk_…) are read-only
and are rejected on any POST, so there is no key you can safely put in page
source. If you want a browser-side form, use the widget above — it exists
precisely so you don't have to proxy this endpoint yourself.
The tenant is resolved from the key — you do not send it. A client-supplied
x-tenant-id header is stripped at the gateway and will not be honoured.
Body:
{
"email": "reader@example.com",
"name": "Reader Name",
"tags": ["website", "blog"],
"source": "homepage"
}| Field | Required | Notes |
|---|---|---|
email |
Yes | The subscriber's email address |
name |
No | Display name; used in {{ subscriber.name }} template variable |
tags |
No | String array; used for segment targeting |
source |
No | Free-text label for tracking where the signup came from |
metadata |
No | Any flat or nested JSON object; accessible as {{ subscriber.metadata.key }} |
redirect |
No | For plain HTML <form> posts. Only honoured when its origin matches the submitting page. |
Success (200):
{ "ok": true }NOTE
The response is deliberately identical whether the address was newly added, already subscribed, previously bounced, or inside the confirmation-resend cooldown. Otherwise the endpoint would answer "is this person on your list?" for anyone who can call it.
Rate limit (429): 10 subscribe requests per 60 seconds per IP per workspace.
Example
The browser posts to your own endpoint, which holds the secret key:
// POST /api/subscribe — your server
export async function POST(request) {
const { email, name, source } = await request.json();
const res = await fetch("https://api.vlozi.app/newsletter/public/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Secret key — server-side only. NOT NEXT_PUBLIC_*, that prefix ships it
// to the browser.
"x-api-key": process.env.VLOZI_API_KEY,
},
body: JSON.stringify({ email, name, source }),
});
return new Response(await res.text(), {
status: res.status,
headers: { "Content-Type": "application/json" },
});
}Because your endpoint faces the internet, add your own abuse controls there. Vlozi's limit is a backstop, not a substitute.
The double opt-in flow
- Visitor submits the form
- Vlozi creates the subscriber with
status=activeandconfirmed_at=null - A confirmation email is sent from your configured sender
- Visitor clicks Confirm →
confirmed_atis set - They are now eligible to receive campaigns
Unconfirmed subscribers are excluded from every campaign send. The confirmation link is valid for 7 days, and they can re-request one at any time.
NOTE
Double opt-in applies to every path — dashboard, import, MCP
add_subscriber — so an upload is never treated as consent. Bulk imports
deliberately do not blast confirmation emails; use a re-permission campaign
for that.
Re-adding someone who previously unsubscribed also returns them to unconfirmed. An opt-out is a standing instruction, not a lapsed one.
You can switch to single opt-in under Newsletter → Settings → Consent, but that is an explicit choice and it does not apply to the widget — anonymous browser signups always confirm.
Unsubscribe links
Every email includes an {{ unsubscribe_url }} token that resolves to a unique,
HMAC-signed link. Clicking it unsubscribes immediately.
<p style="color: #999; font-size: 12px;">
Don't want these emails?
<a href="{{ unsubscribe_url }}">Unsubscribe</a>
</p>WARNING
Vlozi does not block a campaign that omits {{ unsubscribe_url }}. The
editor shows a deliverability warning, but the send proceeds. Leaving it out is
a CAN-SPAM problem and a fast route to spam complaints.
One-click unsubscribe headers (RFC 8058) are attached automatically, so Gmail and Yahoo always show their own control — but that is not a substitute for a link in the body.