On this page

Developer docs

Collect signups from any site with a single POST. Create a campaign, add the domains you'll embed on, then point a form at your subscribe endpoint.

Need field-level detail? See the API reference. Machine-readable OpenAPI: /api/openapi.json.

Set up with an AI agent

Building your form with Lovable, ChatGPT, Claude, or another AI coding tool? Copy this prompt and paste it in — it points the agent at our machine-readable spec and your campaign id.

Prompt for an AI agent
I'm integrating a signup form with Simple Signups, a hosted signup/waitlist backend (not a form builder — I bring my own HTML/UI).
Before building, read the integration spec at https://simple-signups.com/llms.txt (or the human docs at https://simple-signups.com/docs) — it has the exact endpoint, accepted request fields, and error codes. Don't invent any fields that aren't listed there.
My campaign id: pub_your_campaign_id
Build a form (plain HTML or a component, whichever fits my project) that POSTs to the subscribe endpoint from that spec using my campaign id above, includes the honeypot field, and handles the documented success and error responses.

Quickstart

  1. 1Create a campaign and add the domains you'll embed on.
  2. 2Copy your campaign id (starts with pub_) into the field below.
HTML form (no JavaScript)
<form action="https://simple-signups.com/api/subscribe" method="POST">
<input type="hidden" name="campaignId" value="pub_your_campaign_id" />
<input type="email" name="email" required placeholder="you@example.com" />
<!-- Optional tags: comma/semicolon/pipe-separated slugs -->
<input type="hidden" name="tags" value="newsletter" />
<input type="text" name="hp" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px" aria-hidden="true" />
<button type="submit">Subscribe</button>
</form>

Subscribe endpoint

Send a POST request to create a new subscriber. Accepts application/json or application/x-www-form-urlencoded. All requests must include your campaign ID.

POSThttps://simple-signups.com/api/subscribe

FieldRequiredDescription
campaignIdYesYour public campaign id (starts with pub_).
emailYesThe subscriber email address.
firstNameNoOptional given name.
lastNameNoOptional family name.
tagsNoArray of short category slugs, or a comma/semicolon/pipe string. Max 20; re-subscribe merges (unions) tags.
metadataNoObject of extra string/number/boolean fields (size-capped).
redirectNoNo-JS success URL; must be an allowed domain.
hpNoHoneypot — leave empty; filled requests are dropped.
turnstileTokenIf enabledBot-challenge token (only when bot protection is enabled).

Full field catalogue and error codes can be found in the API reference.

Examples

curl -X POST https://simple-signups.com/api/subscribe \
-H 'content-type: application/json' \
-d '{"campaignId":"pub_your_campaign_id","email":"jane@example.com","tags":["newsletter"]}'

Path-style endpoint

Prefer a tidy URL? This takes the campaign id from the path, so the body only needs the email (and any optional fields). A campaignId in the body is allowed but must match the path.

POSThttps://simple-signups.com/c/:publicCampaignId

cURL (path-style)
curl -X POST https://simple-signups.com/c/pub_your_campaign_id \
-H 'content-type: application/json' \
-d '{"email":"jane@example.com","tags":["newsletter"]}'

Tags

Optional tags categorize contacts (e.g. vip, press). Values are normalized to lowercase slugs (max 32 chars, max 20 per contact). On re-subscribe, incoming tags are merged with existing ones — omit the field to leave tags unchanged. Owners can also add/remove tags from the dashboard. Prefer tags for owner categories; use metadata for free-form key/values from the form.

Subscribe with tags
// JSON body — array of slugs (preferred)
await fetch('https://simple-signups.com/api/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
campaignId: 'pub_your_campaign_id',
email: 'jane@example.com',
tags: ['waitlist', 'beta'],
}),
});
// HTML form — comma-separated string is fine
// <input type="hidden" name="tags" value="waitlist,beta" />
// Re-subscribe merges (unions) tags with any existing ones on that contact.

Metadata & extra fields

Attach an optional metadata object of string, number, or boolean values. It's stored with the subscriber, shown in the dashboard, and included in CSV/JSON exports. Keys and total size are capped.

JavaScript (fetch) with metadata
await fetch('https://simple-signups.com/api/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
campaignId: 'pub_your_campaign_id',
email: 'jane@example.com',
tags: ['customer'],
metadata: { plan: 'pro', referredBy: 'newsletter', seats: 3 },
}),
});

Export subscribers

Your list is yours. Download a campaign's subscribers as CSV or JSON from the dashboard at any time, on every plan. Exports include tags and metadata. Exporting a filtered segment (by search, status, or tag) is a Pro feature.

To export from a script, call GET /c/:publicCampaignId/subscribers/export with an API key that has the export:campaign scope (see API keys). Add ?format=json for JSON; CSV is the default.

Analytics

Each campaign has a stats view in the dashboard: daily submission attempts, how many were stored and how many were blocked, grouped by reason (spam and bots, wrong site, bad email), plus the confirmation rate for double opt-in and top referrers. Blocked attempts are counted without storing the submitted email.

Free plans show the last 7 days of history and Pro shows 30 days. Pro can also download the daily series as CSV.

API keys

Create scoped ss_... keys in your dashboard under Integrations. Send them as a Bearer token. Each key is shown once and can be revoked at any time. Scopes: submit:campaign, read:subscribers, export:campaign, write:subscribers (tag PATCH / bulk-tags).

cURL with an API key
# Submit with an API key (scope: submit:campaign)
curl -X POST https://simple-signups.com/c/pub_your_campaign_id \
-H 'authorization: Bearer ss_your_token' \
-H 'content-type: application/json' \
-d '{"email":"jane@example.com","tags":["api","newsletter"]}'
# Read subscribers (scope: read:subscribers) — optional ?tag=vip filter
curl 'https://simple-signups.com/c/pub_your_campaign_id/subscribers?tag=newsletter' \
-H 'authorization: Bearer ss_your_token'
# Export a segment (scope: export:campaign)
curl 'https://simple-signups.com/c/pub_your_campaign_id/subscribers/export?format=csv&tag=newsletter' \
-H 'authorization: Bearer ss_your_token'
# Patch tags on one contact (scope: write:subscribers)
curl -X PATCH https://simple-signups.com/c/pub_your_campaign_id/subscribers/sub_... \
-H 'authorization: Bearer ss_your_token' \
-H 'content-type: application/json' \
-d '{"addTags":["vip"],"removeTags":["waitlist"]}'
# Bulk add/remove tags (scope: write:subscribers)
curl -X POST https://simple-signups.com/c/pub_your_campaign_id/subscribers/bulk-tags \
-H 'authorization: Bearer ss_your_token' \
-H 'content-type: application/json' \
-d '{"ids":["sub_..."],"addTags":["vip"]}'

Webhooks

Register outbound webhooks per campaign to receive a signed POST on subscriber.created and subscriber.confirmed. Each delivery carries an X-Simple-Signups-Signature header (t= timestamp, v1= HMAC-SHA256 of timestamp.body) signed with the secret shown once at creation. A slack kind posts a readable message to a Slack Incoming Webhook.

Verify a webhook signature (Node)
// Verify X-Simple-Signups-Signature (t=timestamp,v1=hex hmac-sha256)
import { createHmac } from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
return expected === parts.v1;
}

Email tools

Connect Buttondown, Kit, Loops, Resend or a self-hosted Mailcheap to a campaign from its Integrations tab and every confirmed subscriber is added there automatically — already confirmed, so the tool doesn't send its own opt-in email (Mailcheap newsletters that confirm signups are the exception). Pick an optional list, segment or tag to add them to.

  • API keys are checked with the provider when you save them, stored encrypted, and never shown again.
  • Failed syncs retry with backoff for about an hour; the campaign shows the last error (for example a revoked key) and each tool has an activity log.
  • Deleting a subscriber here removes them from the chosen list, segment or tag, or unsubscribes them if none is set. Contacts are never deleted from your email tool. Mailcheap has no removal API, so remove them there directly.

Where to create the key: Buttondown API key · Kit API key · Loops API key · Resend API key · Mailcheap API key

Error codes

CodeHTTPWhen
VALIDATION_ERROR400Missing/invalid fields or malformed body.
EMAIL_UNDELIVERABLE400Email domain has no usable MX records.
DOMAIN_NOT_ALLOWED403Origin/redirect not in the allow-list.
BOT_CHECK_FAILED403Bot-challenge verification failed.
UNAUTHORIZED401Missing or invalid API key on an authenticated endpoint.
FORBIDDEN403API key lacks the scope required for the endpoint.
CAMPAIGN_NOT_FOUND404Unknown campaignId.
PAYLOAD_TOO_LARGE413Body exceeds the 4 KiB cap.
RATE_LIMITED429Per-IP or per-campaign rate limit hit.
TOKEN_INVALID410Confirmation token expired or already used.
INTERNAL500Unexpected server error.

Anti-abuse & domains

  • Domain allow-list: requests are accepted only from a campaign's configured domains (checked via Origin/Referer); others get DOMAIN_NOT_ALLOWED.
  • Honeypot: include the hidden hp field; filled submissions are silently discarded.
  • Rate limits: per-IP and per-campaign; back off on RATE_LIMITED using the Retry-After header.
  • Turnstile (Pro, opt-in per campaign): off by default so it never blocks a campaign that hasn't set it up. Turn it on from a campaign's settings and we automatically add that campaign's allowed domains to the shared Turnstile widget's allow-list — the campaign's Setup tab then shows the widget snippet to add to your form. When it's on, send the widget's token as turnstileToken; verification failures return BOT_CHECK_FAILED.
  • Double opt-in: confirmation-required campaigns email a link; subscribers stay pending until confirmed.