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/subscribeandPOST /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.
| Field | Required | Description |
|---|---|---|
| campaignId | Yes | Your public campaign id (starts with pub_). |
| Yes | The subscriber email address. | |
| firstName | No | Optional given name. |
| lastName | No | Optional family name. |
| tags | No | Array of short category slugs, or a comma/semicolon/pipe string. Max 20; re-subscribe merges (unions) tags. |
| metadata | No | Object of extra string/number/boolean fields (size-capped). |
| redirect | No | No-JS success URL; must be an allowed domain. |
| hp | No | Honeypot — leave empty; filled requests are dropped. |
| turnstileToken | If enabled | Cloudflare Turnstile token. |
Examples
<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 -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"]}'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'], }),});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 -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.
{ "success": true, "status": "pending"}Error codes
Failures use a stable error.code enum. Prefer branching on the code, not the message string.
| Code | HTTP | When |
|---|---|---|
| VALIDATION_ERROR | 400 | Missing/invalid fields or malformed body. |
| EMAIL_UNDELIVERABLE | 400 | Email domain has no usable MX records. |
| DOMAIN_NOT_ALLOWED | 403 | Origin/redirect not in the allow-list. |
| BOT_CHECK_FAILED | 403 | Turnstile verification failed. |
| UNAUTHORIZED | 401 | Missing or invalid API key on an authenticated endpoint. |
| FORBIDDEN | 403 | API key lacks the scope required for the endpoint. |
| CAMPAIGN_NOT_FOUND | 404 | Unknown campaignId. |
| PAYLOAD_TOO_LARGE | 413 | Body exceeds the 4 KiB cap. |
| RATE_LIMITED | 429 | Per-IP or per-campaign rate limit hit. |
| TOKEN_INVALID | 410 | Confirmation token expired or already used. |
| INTERNAL | 500 | Unexpected 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.
# 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 filtercurl '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 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;}