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.

Quickstart

  1. Create a campaign in your dashboard and copy its public id.
  2. Add each website domain you'll embed on to the allow-list.
  3. Paste the HTML form below (swap in your campaign id) and submit a test.
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

POST https://simple-signups.com/api/subscribe — accepts JSON or application/x-www-form-urlencoded. Full field catalogue and error codes live on the API reference.

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).

Examples

cURL
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"]}'
JavaScript (fetch)
await fetch('https://simple-signups.com/api/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
campaignId: 'pub_your_campaign_id',
email,
tags: ['newsletter'],
}),
});
React
async function onSubmit(e) {
e.preventDefault();
await fetch('https://simple-signups.com/api/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
campaignId: 'pub_your_campaign_id',
email: e.currentTarget.email.value,
tags: ['newsletter'],
}),
});
}

Path-style endpoint

Prefer a tidy URL? POST /c/:publicCampaignId 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.

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 },
}),
});

API keys

Create scoped ss_live_... 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_live_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_live_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_live_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_live_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_live_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;
}

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.
  • Bot challenge: when enabled, send the challenge token; verification failures return BOT_CHECK_FAILED.
  • Double opt-in: confirmation-required campaigns email a link; subscribers stay pending until confirmed.