Developer docsAPI reference

API reference

Canonical request and response fields, error codes, and copy-paste clients for the public subscribe API. For a guided walkthrough, see the developer docs. Machine-readable contract: /api/openapi.json.

Auth model

  • Public subscribe (POST /api/subscribe and POST /c/:publicCampaignId): no API key. Browser origins must be on the campaign domain allow-list.
  • API key (Authorization: Bearer ss_live_…): required for listing, export, and tag management on /c/:id/… routes. Scopes are granted per key.
  • Dashboard / session cookie: campaign and account management only — not used by public forms.

Subscribe endpoint

POST https://simple-signups.com/api/subscribe — accepts JSON or application/x-www-form-urlencoded.

Path-style alias: POST /c/:publicCampaignId — campaign id from the path; body only needs email (and optional fields). A body campaignId is allowed but must match the path.

Request fields

Do not invent fields. Only these are accepted on the public subscribe endpoints.

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 enabledCloudflare Turnstile token.

Examples

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

Success response

Successful subscribe returns JSON. Status may be pending when double opt-in is required, otherwise confirmed.

JSON
{
"success": true,
"status": "pending"
}

Error codes

Failures use a stable error.code enum. Prefer branching on the code, not the message string.

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

Notes for AI coding agents

Minimal correct client checklist when generating an integration:

  • Do not invent request fields. Only the fields in the table above are accepted.
  • Send JSON (`content-type: application/json`) or `application/x-www-form-urlencoded`.
  • Always include `campaignId` (or use the path-style URL) and `email`.
  • Treat `error.code` as a stable enum — branch on codes, not message text.
  • On `RATE_LIMITED` (429), back off using the `Retry-After` header.
  • Prefer `https://simple-signups.com` (or the deployment host) as a single base URL variable.
  • Machine-readable contract: GET /api/openapi.json

API keys

Create scoped ss_live_… keys in the dashboard under Integrations. Send them as a Bearer token. Each key is shown once and can be revoked. Scopes: submit:campaign, read:subscribers, export:campaign, write:subscribers.

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

Outbound webhooks per campaign fire on subscriber.created and subscriber.confirmed. Deliveries include X-Simple-Signups-Signature (t= timestamp, v1= HMAC-SHA256 of timestamp.body).

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