API reference
Signup forms
Signup forms, their URLs and submissions from your own HTML
Signup forms explained. Create one form per newsletter, then post to it from your own HTML or load its script.
The form object
{
"id": "3f2a9c1e-7b4d-4e8a-a6c2-1d9e5b7f0a34",
"name": "Blog newsletter",
"listIds": ["5a8e2c1f-0b3d-4e6a-9f7c-2d1e8b4a6c3f"],
"doubleOptIn": true,
"askFirstName": false,
"title": "Get new posts by email",
"description": null,
"buttonLabel": "Subscribe",
"successMessage": "Thanks! Check your inbox to confirm your subscription.",
"redirectUrl": null,
"confirmationSubject": "Confirm your subscription",
"hostedUrl": "https://link.acme.com/f/3f2a9c1e-7b4d-4e8a-a6c2-1d9e5b7f0a34",
"embedScriptUrl": "https://link.acme.com/f/3f2a9c1e-7b4d-4e8a-a6c2-1d9e5b7f0a34/embed.js",
"createdAt": "2026-10-01T12:00:00Z",
"updatedAt": "2026-10-01T12:00:00Z"
}
| Field | Meaning |
|---|---|
hostedUrl | Hosted page of the form, and the submission URL |
embedScriptUrl | Script that draws the form inside any page |
Both live on the tracking domain of the space and are null until the space has a sending domain. The form answers once the tracking DNS record is in place; confirmation emails wait for the domain to be verified.
List forms
GET /v1/forms
Every form of the space, oldest first.
curl https://api.kulamail.com/v1/forms \
-H "Authorization: Bearer ml_your_api_key"
Create a form
POST /v1/forms
name
string required
120 characters at most. Internal, visitors don't see it.
listIds
string[] required
Lists of the space the form subscribes to, at least one.
doubleOptIn
boolean
Send a confirmation email before subscribing. Default
true.askFirstName
boolean
Add a first name field. Default
false.title
string
Heading of the hosted page and the script. 200 characters at most.
description
string
Text under the heading. 1,000 characters at most.
buttonLabel
string
60 characters at most. Default
Subscribe.successMessage
string
Shown after a submission. 500 characters at most.
redirectUrl
string
https:// or http:// page the visitor goes to after a submission, instead of the success message.confirmationSubject
string
Subject of the confirmation email. 200 characters at most.
curl https://api.kulamail.com/v1/forms \
-H "Authorization: Bearer ml_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Blog newsletter", "listIds": ["5a8e2c1f-0b3d-4e6a-9f7c-2d1e8b4a6c3f"] }'
Returns 201 with the form object. A list of another space answers 404.
Get, update and delete a form
GET /v1/forms/{id}returns the form.PATCH /v1/forms/{id}changes any field of Create a form. Fields left out stay as they are; an emptytitle,descriptionorredirectUrlclears it.DELETE /v1/forms/{id}deletes the form: it stops taking submissions. Contacts and subscriptions stay. Answers204.
Post from your own form
Keep your own HTML and post to hostedUrl from the reader's browser:
<form method="post" action="https://link.acme.com/f/3f2a9c1e-7b4d-4e8a-a6c2-1d9e5b7f0a34">
<input type="email" name="email" required>
<input type="text" name="first_name">
<!-- Leave hidden and empty: it catches bots -->
<input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px">
<button type="submit">Subscribe</button>
</form>
| Field | Meaning |
|---|---|
email | Required |
first_name | Kept when askFirstName is true |
website | Honeypot: keep it empty and hidden. A filled one is answered as a success and dropped |
The body is form-encoded or JSON, and any origin is allowed (CORS). With Accept: application/json, the answer is:
{ "ok": true, "pending": true, "message": "Thanks! Check your inbox to confirm your subscription." }
pending is true while the reader has to confirm by email.
Submissions must come from the reader's browser, not be relayed by your server: Kula allows 10 submissions per minute per IP address. To create contacts from your backend, use the contacts API.