Integration Guide

Add a newsletter signup form to your static site or serverless frontend in minutes. Pick your framework and copy the snippet — most setups need no backend.

How it works

New to emcognito? Learn how it works on our homepage, or create a free account to get started.

emcognito exposes a single public endpoint: POST https://api.ec.emcognito.com/subscribe. Send a JSON body with an email field and include your X-Publishable-Key header. That's it — the subscriber is recorded as pending, sent a confirmation email, and scoped to your account after they confirm. Get your publishable key from the owner portal after signing up.

Lead metadata: optional name, provider, spend_band, region, workload, and notes fields let you qualify signups without running a separate intake API. They appear in the portal, CSV export, REST API responses, and webhook payloads.

Double opt-in: emcognito sends the confirmation email and hosts the confirmation page — you don't build either. Subscribers stay pending until they click the link; a subscriber.confirmed webhook (or status: "verified" from the developer API) is your signal that they're verified.

One-click unsubscribe: every email carries List-Unsubscribe / List-Unsubscribe-Post headers (RFC 8058) and a hosted unsubscribe page — the bulk-sender requirement Gmail and Yahoo enforce. When someone opts out we hard-delete them and fire a subscriber.unsubscribed webhook so your synced copy stays clean. You build nothing.

The publishable key (pk_live_…) is safe in client-side code, so the framework snippets below run in the browser. The server-side examples (Next.js, Nuxt, SvelteKit, Astro) are optional — use them only if you prefer to keep the key out of your bundle.

POSThttps://api.ec.emcognito.com/subscribe
FieldTypeDescription
emailstringThe subscriber's email address (required)
namestringThe subscriber's name (optional)
providerstringProvider, platform, or vendor the lead uses (optional)
spend_bandstringBudget or usage band for qualification (optional)
regionstringLead geography or target market (optional)
workloadstringPrimary use case or workload (optional)
notesstringFree-form lead notes, up to 1,000 characters (optional)
HeaderDescription
X-Publishable-KeyYour account's publishable API key (safe to include in client-side code)
index.html
<form id="subscribe-form">
  <input type="email" id="email-input" placeholder="you@example.com" required />
  <button type="submit">Subscribe</button>
  <p id="subscribe-msg"></p>
</form>

<script>
  document.getElementById('subscribe-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const email = document.getElementById('email-input').value;
    const msg = document.getElementById('subscribe-msg');

    try {
      const res = await fetch('https://api.ec.emcognito.com/subscribe', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Publishable-Key': 'YOUR_PUBLISHABLE_KEY',
        },
        body: JSON.stringify({
          email,
          // Optional lead metadata:
          // name, provider, spend_band, region, workload, notes
        }),
      });
      if (!res.ok) throw new Error(await res.text());
      msg.textContent = 'Check your inbox to confirm your subscription.';
    } catch (err) {
      msg.textContent = 'Something went wrong. Please try again.';
    }
  });
</script>
No dependencies required. Works in any plain HTML page — GitHub Pages, Netlify, any static host.

Developer API & webhooks

Beyond collecting subscribers, you can read your list from your own server and receive real-time events — so confirmed subscribers flow straight into your ESP, CRM, or data warehouse. Create secret keys and webhook endpoints on the Developers page in your portal.

KeyTypeWhere it livesUsed for
pk_live_…PublishableSafe in client-side codePOST /subscribe
sk_live_…SecretServer-side only — never in a browser/v1/* read & write

Read subscribers (REST)

Authenticate with a server-side secret key (sk_live_…). The origin is derived from the key, so a key can only read its own site's subscribers. Secret keys are server-side only — never expose one in a browser.

  • GET /v1/subscribers — list (status, limit, starting_after)
  • GET /v1/subscribers/{email} — fetch one
  • POST /v1/subscribers — add one (status: subscribed or pending; accepts the same optional lead metadata as /subscribe)
  • DELETE /v1/subscribers/{email} — remove one
Authenticated request
curl https://api.ec.emcognito.com/v1/subscribers \
  -H "Authorization: Bearer sk_live_..."

Webhook events

We POST a signed JSON event to your endpoint for each of:

  • subscriber.confirmed — completed double opt-in
  • subscriber.created — signed up, not yet confirmed
  • subscriber.deleted — removed from the list
  • subscriber.unsubscribed — self-removed via one-click unsubscribe
  • subscriber.bounced — address permanently bounced
  • subscriber.complained — marked as spam

Each delivery carries Emcognito-Signature: t=<unix>,v1=<hex>. Verify it by recomputing HMAC-SHA256(secret, "{t}.{raw_body}") over the raw body, comparing in constant time, and rejecting stale timestamps. Failed deliveries are retried with backoff; endpoints auto-disable after repeated failures.

Verify in Node.js
import crypto from 'node:crypto'

const header = req.get('Emcognito-Signature') || ''   // "t=...,v1=..."
const p = Object.fromEntries(header.split(',').map(s => s.split('=')))
const expected = crypto
  .createHmac('sha256', process.env.EMCOGNITO_WEBHOOK_SECRET)
  .update(p.t + '.' + rawBody)        // rawBody = the unparsed request body
  .digest('hex')
const ok = p.v1 && crypto.timingSafeEqual(
  Buffer.from(expected), Buffer.from(p.v1))
if (!ok || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) reject()
Verify in Python
import hmac, hashlib, time

header = request.headers.get("Emcognito-Signature", "")
p = dict(kv.split("=", 1) for kv in header.split(",") if "=" in kv)
expected = hmac.new(SECRET, f'{p["t"]}.{raw_body}'.encode(),
                    hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, p.get("v1", "")):
    reject()
if abs(time.time() - int(p["t"])) > 300:
    reject()  # stale timestamp

Errors & status codes

Every error response is JSON: { "detail": "<message>" }. Handle these in your integration:

POST /subscribe

StatusMeaning
201Accepted — confirmation email sent; the subscriber stays pending until they click it (double opt-in)
400X-Publishable-Key header missing
403Publishable key not recognized
422Invalid email address
503Temporary backend/email error — safe to retry

Developer API (/v1/*, secret key)

StatusMeaning
401Missing, invalid, or revoked secret key
403Key is missing the required scope (subscribers:read / subscribers:write)
404No subscriber with that email
400Invalid cursor or request body
503Temporary backend error — safe to retry

Rate limits: /subscribe and the /v1 endpoints are not per-identity rate limited (a pending address is re-emailed at most once per 60 s but still returns 201). The account-email endpoints — signup and magic-link request — are throttled per email and may return 429 with no Retry-After header, so back off and retry on a fixed delay.

Ready to start collecting subscribers?

Create a free account to get your publishable key. Free during beta.

Create free account