POST /api/v1/signup-pages
Creates a sign-up page. Answers 201 with the created page — its id is what every other sign-up
page call takes.
Without status, the page is a draft: it answers a not-found page at its address and collects
nothing until you publish it. Provisioning never puts an address online by accident.
Authorization
| Role in the organization | This endpoint |
|---|---|
OWNER | Allowed |
ADMIN | Allowed |
MEMBER | Refused — 403 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Internal name, 1 to 100 characters. Only your team sees it |
slug | string | yes | Last part of the public address. 2 to 100 characters, lowercase letters, digits and hyphens only. Unique in the organization |
listId | string | yes | The list that receives the subscribers. Must belong to your organization |
content | string | yes | The HTML of the page. Must carry {{form}} |
heroTitle | string | yes | The title visitors read, up to 200 characters |
submitLabel | string | yes | Label of the sign-up button, up to 60 characters |
successMessage | string | yes | Shown after a sign-up when there is no redirect, up to 300 characters |
sourceTemplateName | string | no | The theme the HTML was copied from, up to 100 characters |
heroSubtitle | string | no | Subtitle under the title, up to 300 characters |
redirectUrl | string | no | http:// or https:// address to send the visitor to instead of showing successMessage |
collectFirstName | boolean | no | true by default |
collectLastName | boolean | no | false by default |
status | string | no | draft (default) or published |
json
{
"name": "Newsletter footer",
"slug": "newsletter",
"listId": "9f8e7d6c-1111-4bbb-8ccc-ddddeeeeffff",
"content": "<html><body><h1>{{heroTitle}}</h1>{{form}}{{poweredBy}}</body></html>",
"heroTitle": "Join the list",
"submitLabel": "Sign me up",
"successMessage": "Check your inbox.",
"status": "published"
}Example
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"name":"Newsletter footer","slug":"newsletter","listId":"'$LIST_ID'","content":"<html><body>{{form}}</body></html>","heroTitle":"Join the list","submitLabel":"Sign me up","successMessage":"Check your inbox."}' \
"https://www.agentsmail.io/api/v1/signup-pages"Response
201 Created— the created page,contentincluded.400 Bad Request— a required field is missing or malformed, or the HTML carries no{{form}}. When the shape is at fault,detailscarries the field-level issues.401 Unauthorized— missing or invalid key, or its owner left the organization.403 Forbidden— the key's owner is amemberof the organization, not anadmin.404 Not Found—listIdnames no list of your organization. Unknown and not-yours answer the same thing.409 Conflict— another page already holds thatslug; the message names it. Nothing was created.429 Too Many Requests— the key is over its rate limit.
Response fields
| Field | Type | Description |
|---|---|---|
success | boolean | Indicates if the operation was successful |
data.id | string | Unique identifier for the page |
data.name | string | Internal name |
data.slug | string | Last part of the public address |
data.status | string | draft or published |
data.listId | string | The list that receives the subscribers |
data.content | string | The HTML of the page |
data.sourceTemplateName | string | null | The theme it was copied from, null for pasted HTML |
data.heroTitle | string | The title visitors read |
data.heroSubtitle | string | null | The subtitle, null when unset |
data.submitLabel | string | Label of the sign-up button |
data.successMessage | string | Shown after a sign-up when there is no redirect |
data.redirectUrl | string | null | Where the visitor goes after signing up, null for none |
data.collectFirstName | boolean | Whether the form asks for a first name |
data.collectLastName | boolean | Whether the form asks for a last name |
data.createdAt | string | ISO creation date |
data.updatedAt | string | ISO date of the last change |
Example response
json
{
"success": true,
"data": {
"id": "3d3d3d3d-4444-4eee-9fff-000011112222",
"name": "Newsletter footer",
"slug": "newsletter",
"status": "published",
"listId": "9f8e7d6c-1111-4bbb-8ccc-ddddeeeeffff",
"content": "<html><body>{{form}}</body></html>",
"sourceTemplateName": null,
"heroTitle": "Join the list",
"heroSubtitle": null,
"submitLabel": "Sign me up",
"successMessage": "Check your inbox.",
"redirectUrl": null,
"collectFirstName": true,
"collectLastName": false,
"createdAt": "2026-08-23T10:22:05.000Z",
"updatedAt": "2026-08-23T10:22:05.000Z"
}
}Agent recipe
Copy a theme, then publish once the address is confirmed — three calls, in this order:js
const themes = (await get('/api/v1/signup-page-templates')).data
const theme = themes[0]
const page = (
await post('/api/v1/signup-pages', {
name: 'Newsletter footer',
slug: 'newsletter',
listId,
content: theme.content,
sourceTemplateName: theme.name,
heroTitle: 'Join the list',
submitLabel: 'Sign me up',
successMessage: 'Check your inbox.',
})
).data
// The page is still a draft here: nothing answers at its address yet
await patch(`/api/v1/signup-pages/${page.id}`, {status: 'published'})409 on the second call means the slug is taken. Pick another one — retrying will not help.
Notes
- The HTML is a copy, never a link. Changing the theme in the catalogue later leaves your page exactly as it is.
redirectUrlandsuccessMessageare not alternatives you both set: a page with a redirect never shows the message.