Getting started

Adding a subscribe form

Embed a subscribe form on your website so visitors can join your newsletter.

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/subscribe

Headers:

Content-Type: application/json
x-api-key: ls_your_secret_key

Create 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

  1. Visitor submits the form
  2. Vlozi creates the subscriber with status=active and confirmed_at=null
  3. A confirmation email is sent from your configured sender
  4. Visitor clicks Confirmconfirmed_at is set
  5. 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.

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.

Newsletter · Getting startedEdit on GitHub