{"_id":"@codesocietyou/contentedge-newsletter-sdk","_rev":"5-14396175232fc531864ed8fe18a9dd4e","name":"@codesocietyou/contentedge-newsletter-sdk","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@codesocietyou/contentedge-newsletter-sdk","version":"0.1.0","keywords":["contentedge","newsletter","sdk"],"license":"UNLICENSED","_id":"@codesocietyou/contentedge-newsletter-sdk@0.1.0","maintainers":[{"name":"mariosciro","email":"info@paraplu.cloud"},{"name":"maxit789","email":"tijanalambic@gmail.com"},{"name":"nikobene","email":"benediktnikolai@gmail.com"},{"name":"loveeykb","email":"karirloveleen@gmail.com"}],"homepage":"https://github.com/ParapluOU/contentedge-newsletter-sdk#readme","bugs":{"url":"https://github.com/ParapluOU/contentedge-newsletter-sdk/issues"},"dist":{"shasum":"8cd9d4c92970c694615c3e0abe243a5022949fb8","tarball":"https://registry.npmjs.org/@codesocietyou/contentedge-newsletter-sdk/-/contentedge-newsletter-sdk-0.1.0.tgz","fileCount":6,"integrity":"sha512-urV5ikeTmNEwQy0d6ITxGjSSq8c7rWx8PWI1PooqITQ4ShvjEQyi9w6pYMsAi3rpaRSVhDGsICKKBV6Sn4DeXg==","signatures":[{"sig":"MEUCIAsCzRgV5PntBg7v4rcmzUK+uHroPokxz++4zcIP0SSyAiEA6lDCzn850RDlFLsIX9LveF1TcqSXTl2c3OX5IpLs8Wo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":10031},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"fb5362cfda4ab7311d363f91efb122738d7e67fd","scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"mariosciro","email":"info@paraplu.cloud"},"repository":{"url":"git+https://github.com/ParapluOU/contentedge-newsletter-sdk.git","type":"git"},"_npmVersion":"11.8.0","description":"Framework-agnostic browser SDK for ContentEdge newsletter public forms.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"latest","vitest":"^4.1.6","typescript":"latest"},"_npmOperationalInternal":{"tmp":"tmp/contentedge-newsletter-sdk_0.1.0_1778541944068_0.6776823061393122","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@codesocietyou/contentedge-newsletter-sdk","version":"0.1.1","keywords":["contentedge","newsletter","sdk"],"license":"MIT","_id":"@codesocietyou/contentedge-newsletter-sdk@0.1.1","maintainers":[{"name":"mariosciro","email":"info@paraplu.cloud"},{"name":"maxit789","email":"tijanalambic@gmail.com"},{"name":"nikobene","email":"benediktnikolai@gmail.com"},{"name":"loveeykb","email":"karirloveleen@gmail.com"}],"homepage":"https://github.com/ParapluOU/contentedge-newsletter-sdk#readme","bugs":{"url":"https://github.com/ParapluOU/contentedge-newsletter-sdk/issues"},"dist":{"shasum":"dccfeacc813055ea25d8a2ac389b4e1e7ad29ae7","tarball":"https://registry.npmjs.org/@codesocietyou/contentedge-newsletter-sdk/-/contentedge-newsletter-sdk-0.1.1.tgz","fileCount":8,"integrity":"sha512-jdJqRhA4B6+4/0oRdvWW0/wny3BRfIFu4Vco6Qdn3EUembOetPhmORrChu7z7y+WhvMGARUDriGSo1Kbrw1NtA==","signatures":[{"sig":"MEQCIG1STgGRONTDl+DKg82LbvdwbXuH0hmAdKNLfxVZdoo6AiBd5KS+q2UtJX/JMobnxkbF7YXNG4p5CZZ3Ly4G9CUmsQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@codesocietyou%2fcontentedge-newsletter-sdk@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":10260},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"526c21e2a6ebedb6c05ec04d84158167b8903b6a","scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:acf9b71d-9fa2-4cfe-9957-e8e9e8116b1b"}},"repository":{"url":"git+https://github.com/ParapluOU/contentedge-newsletter-sdk.git","type":"git"},"_npmVersion":"11.8.0","description":"Framework-agnostic browser SDK for ContentEdge newsletter public forms.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"latest","vitest":"^4.1.6","typescript":"latest"},"_npmOperationalInternal":{"tmp":"tmp/contentedge-newsletter-sdk_0.1.1_1778543135957_0.6847211466996008","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@codesocietyou/contentedge-newsletter-sdk","version":"0.1.2","keywords":["contentedge","newsletter","sdk"],"license":"MIT","_id":"@codesocietyou/contentedge-newsletter-sdk@0.1.2","maintainers":[{"name":"mariosciro","email":"info@paraplu.cloud"},{"name":"maxit789","email":"tijanalambic@gmail.com"},{"name":"nikobene","email":"benediktnikolai@gmail.com"},{"name":"loveeykb","email":"karirloveleen@gmail.com"}],"homepage":"https://github.com/ParapluOU/contentedge-newsletter-sdk#readme","bugs":{"url":"https://github.com/ParapluOU/contentedge-newsletter-sdk/issues"},"dist":{"shasum":"eb2f5a1aa0a0aad2964688463aca164e176d8e0a","tarball":"https://registry.npmjs.org/@codesocietyou/contentedge-newsletter-sdk/-/contentedge-newsletter-sdk-0.1.2.tgz","fileCount":8,"integrity":"sha512-oGL6+d/SIeNZm40rRApfWe+32YChRiV63pDlVFGGyKw47pEOFrCiGFtZ/7CTUtsMau4rb72aWmVaBv0BfriSgQ==","signatures":[{"sig":"MEUCIQCMS5LPCclPSHMzxS01yqBdSbP+Uotp3lioiBu3Rx31DwIgQ6bHeXF72wlUZfEhcHMNsEqOEoR/xhlHizUqHihBT20=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@codesocietyou%2fcontentedge-newsletter-sdk@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":15814},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"9aa40424c7dbcd5af2610470ee2425a56b91f3d9","scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:acf9b71d-9fa2-4cfe-9957-e8e9e8116b1b"}},"repository":{"url":"git+https://github.com/ParapluOU/contentedge-newsletter-sdk.git","type":"git"},"_npmVersion":"11.8.0","description":"Framework-agnostic browser SDK for ContentEdge newsletter public forms.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"latest","vitest":"^4.1.6","typescript":"latest"},"_npmOperationalInternal":{"tmp":"tmp/contentedge-newsletter-sdk_0.1.2_1778543867467_0.895645886289693","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@codesocietyou/contentedge-newsletter-sdk","version":"0.1.3","keywords":["contentedge","newsletter","sdk"],"license":"MIT","_id":"@codesocietyou/contentedge-newsletter-sdk@0.1.3","maintainers":[{"name":"mariosciro","email":"info@paraplu.cloud"},{"name":"maxit789","email":"tijanalambic@gmail.com"},{"name":"nikobene","email":"benediktnikolai@gmail.com"},{"name":"loveeykb","email":"karirloveleen@gmail.com"}],"homepage":"https://github.com/ParapluOU/contentedge-newsletter-sdk#readme","bugs":{"url":"https://github.com/ParapluOU/contentedge-newsletter-sdk/issues"},"dist":{"shasum":"7cf47b94ca8701a20729c53eabf45cfe13ab51e1","tarball":"https://registry.npmjs.org/@codesocietyou/contentedge-newsletter-sdk/-/contentedge-newsletter-sdk-0.1.3.tgz","fileCount":8,"integrity":"sha512-mDCPvuJ68WSAiq2g9qlFGw/avPV4LRn28KX9mhGwbQGYKU3x0V/WW10xMZGjRwqG4jFxWsFWVB1JRwf6+kLc8w==","signatures":[{"sig":"MEQCIDqYdbLjn1zOYPulIEKA7ZD2S+0k+gYPLb1arp9L3FjuAiAxtB/vUW9XDZDqLgaWilrHR1GiVq2jlm6EHaHUshlZLQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@codesocietyou%2fcontentedge-newsletter-sdk@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":17312},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"9cabec6b0ae175348b9fd78f974f53fb1220f520","scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:acf9b71d-9fa2-4cfe-9957-e8e9e8116b1b"}},"repository":{"url":"git+https://github.com/ParapluOU/contentedge-newsletter-sdk.git","type":"git"},"_npmVersion":"11.8.0","description":"Framework-agnostic browser SDK for ContentEdge newsletter public forms.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.1.6","typescript":"6.0.3"},"_npmOperationalInternal":{"tmp":"tmp/contentedge-newsletter-sdk_0.1.3_1778784636629_0.3692902076975735","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@codesocietyou/contentedge-newsletter-sdk@0.2.0","bugs":{"url":"https://github.com/ParapluOU/contentedge-newsletter-sdk/issues"},"dist":{"shasum":"89de699a510e1d235c8b9e86e02f4e06cdc49870","tarball":"https://registry.npmjs.org/@codesocietyou/contentedge-newsletter-sdk/-/contentedge-newsletter-sdk-0.2.0.tgz","fileCount":8,"integrity":"sha512-lHM93s2jzSztoeZ+lsSUtunmb7s0jhMvI4/mpi6AgXfphzHdCYlDYlD8bMMdCyGYkEmVgvkOuVN+8AQr938K4Q==","signatures":[{"sig":"MEYCIQDkslx86SaKJRJUysTcmcQem1FZDPfl7i25FGZ7MAYiBAIhAO3XPFG7zWYR6DejvoMXNlTWRQ9g2Z6sQsgo6Ohy80hR","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIF/ectC6+/bYV+TUchJLybIsk60hphigwXkjTldkGgnyAiEA7or7P7zHj/sH6kWX2wIPbZ+4qzkYW3cqOoWFtRYq1gU="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@codesocietyou%2fcontentedge-newsletter-sdk@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":28543},"main":"./dist/index.cjs","name":"@codesocietyou/contentedge-newsletter-sdk","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"5dbfa50d332cf449f2add61b65a5c6a6faa3aed6","license":"MIT","scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:acf9b71d-9fa2-4cfe-9957-e8e9e8116b1b"}},"homepage":"https://github.com/ParapluOU/contentedge-newsletter-sdk#readme","keywords":["contentedge","newsletter","sdk"],"repository":{"url":"git+https://github.com/ParapluOU/contentedge-newsletter-sdk.git","type":"git"},"_npmVersion":"11.8.0","description":"Framework-agnostic browser SDK for ContentEdge newsletter public forms.","directories":{},"maintainers":[{"name":"mariosciro","email":"info@paraplu.cloud"},{"name":"maxit789","email":"tijanalambic@gmail.com"},{"name":"nikobene","email":"benediktnikolai@gmail.com"},{"name":"loveeykb","email":"karirloveleen@gmail.com"}],"_nodeVersion":"24.13.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.1.6","typescript":"6.0.3"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/contentedge-newsletter-sdk_0.2.0_1789941387130_0.0665266408978955"}}},"time":{"created":"2026-05-11T23:25:43.989Z","modified":"2026-09-20T21:56:27.639Z","0.1.0":"2026-05-11T23:25:44.230Z","0.1.1":"2026-05-11T23:45:36.095Z","0.1.2":"2026-05-11T23:57:47.604Z","0.1.3":"2026-05-14T18:50:36.803Z","0.2.0":"2026-09-20T21:56:27.232Z"},"bugs":{"url":"https://github.com/ParapluOU/contentedge-newsletter-sdk/issues"},"license":"MIT","homepage":"https://github.com/ParapluOU/contentedge-newsletter-sdk#readme","keywords":["contentedge","newsletter","sdk"],"repository":{"url":"git+https://github.com/ParapluOU/contentedge-newsletter-sdk.git","type":"git"},"description":"Framework-agnostic browser SDK for ContentEdge newsletter public forms.","maintainers":[{"name":"mariosciro","email":"info@paraplu.cloud"},{"name":"maxit789","email":"tijanalambic@gmail.com"},{"name":"nikobene","email":"benediktnikolai@gmail.com"},{"name":"loveeykb","email":"karirloveleen@gmail.com"}],"readme":"# ContentEdge Newsletter SDK\n\nA lightweight, framework-agnostic TypeScript client for ContentEdge public newsletter forms. It provides a small typed wrapper for subscriptions, public enquiries, confirmation links, and unsubscribe links without requiring a frontend framework.\n\nThis SDK models the public newsletter form contract through `tenant` and `formKey`. It does not import or depend on the ContentEdge server code.\n\n> ### Looking for IT services?\n> <img src=\"https://fromulo.com/codesociety.png\" align=\"left\" width=\"80\" alt=\"CodeSociety\">\n>\n> **[CodeSociety](https://codesocietyhub.com/)** is our consulting & contracting arm — specializing in\n> **IT architecture**, **XML authoring systems**, **FontoXML integration**, and **TerminusDB consulting**.\n> We build structured content platforms and data solutions that power digital publishing.\n>\n> **[Let's talk! &#8594;](https://codesocietyhub.com/contact.html)**\n\n## Features\n\n- **Framework-agnostic**: Works in browser apps and any runtime with `fetch`.\n- **TypeScript-first**: Typed configuration, request payloads, and public responses.\n- **Form introspection**: Read the browser-safe form configuration to render the right fields and consent text.\n- **Public subscriptions**: Submit email, profile fields, consent, and optional verification inputs.\n- **Public enquiries**: Submit structured form fields for contact or enquiry flows.\n- **Token helpers**: Confirm subscriptions and unsubscribe using public tokens.\n- **Custom fetch support**: Inject your own `fetch` implementation for tests or custom request handling.\n- **Cancellation and timeouts**: Pass an `AbortSignal` per call, or set a client-wide `timeoutMs`.\n- **Structured errors**: `ContentEdgeNewsletterError` includes HTTP status and response details when available.\n\n## Installation\n\n```bash\nnpm install @codesocietyou/contentedge-newsletter-sdk\n# or\nyarn add @codesocietyou/contentedge-newsletter-sdk\n# or\npnpm add @codesocietyou/contentedge-newsletter-sdk\n```\n\n## Quick Start\n\n### 1. Create a newsletter client\n\n```typescript\nimport { createNewsletterClient } from \"@codesocietyou/contentedge-newsletter-sdk\";\n\nconst newsletter = createNewsletterClient({\n  baseUrl: \"https://api.contentedgecms.com\",\n  tenant: \"example\",\n  formKey: \"homepage-newsletter\",\n});\n```\n\n### 2. Read the form configuration\n\nOptional, but it lets you render the form from ContentEdge rather than hardcoding it.\n\n```typescript\nconst form = await newsletter.getFormConfig();\n\n// form.purpose            -> \"SUBSCRIPTION\" | \"ENQUIRY\" | \"SUBSCRIPTION_AND_ENQUIRY\"\n// form.consentText        -> the exact consent statement to display\n// form.consentRequired    -> whether `consent: true` is mandatory\n// form.captchaRequired    -> whether you must supply a captcha token\n// form.allowedFields      -> attribute or enquiry field keys the API accepts\n// form.doubleOptInRequired-> whether subscribers must confirm by email\n```\n\n### 3. Subscribe a reader\n\nUse `subscribe` with forms configured in ContentEdge for subscription or subscription and enquiry.\n\n```typescript\nawait newsletter.subscribe({\n  email: \"reader@example.com\",\n  firstName: \"Ada\",\n  lastName: \"Lovelace\",\n  consent: true,\n  captchaToken,\n});\n```\n\n### 4. Submit an enquiry\n\nUse `submitEnquiry` with forms configured in ContentEdge for enquiry or subscription and enquiry.\n\n```typescript\nawait newsletter.submitEnquiry({\n  fields: {\n    email: \"reader@example.com\",\n    name: \"Ada Lovelace\",\n    message: \"I would like to hear more about your newsletter.\",\n  },\n  captchaToken,\n});\n```\n\n### 5. Confirm or unsubscribe\n\nContentEdge already hosts landing pages for both links at `/confirm-subscription` and `/unsubscribe` on the tenant domain, and the confirmation and unsubscribe emails point there by default. You only need these methods when you host your own landing pages.\n\n```typescript\nawait newsletter.confirm(\"confirmation-token\");\nawait newsletter.unsubscribe(\"unsubscribe-token\");\n```\n\n## API Reference\n\n### `createNewsletterClient(config)`\n\nCreate a client bound to a ContentEdge public newsletter form.\n\nThe `formKey` points to an admin-configured public form. That form controls whether the SDK can submit subscriptions, enquiries, or both.\n\n```typescript\ninterface NewsletterClientConfig {\n  baseUrl: string;   // ContentEdge API base URL\n  tenant: string;    // Tenant domain used by the public API\n  formKey: string;   // Public newsletter form key\n  fetch?: typeof fetch;\n  timeoutMs?: number; // Aborts any request that exceeds this duration\n}\n```\n\nEvery method accepts an optional trailing `RequestOptions` argument:\n\n```typescript\ninterface RequestOptions {\n  signal?: AbortSignal;\n}\n```\n\n### `getFormConfig(options?)`\n\nRead the browser-safe configuration of the form this client is bound to. ContentEdge deliberately withholds admin-owned settings: target lists, templates, sender identities, enquiry recipients, and allowed origins are never returned.\n\n```typescript\ninterface PublicFormConfig {\n  formKey: string;\n  purpose: \"SUBSCRIPTION\" | \"ENQUIRY\" | \"SUBSCRIPTION_AND_ENQUIRY\";\n  consentText?: string;\n  consentRequired: boolean;\n  captchaRequired: boolean;\n  doubleOptInRequired: boolean;\n  allowedFields: string[];\n}\n```\n\nThis call is subject to the same allowed-origin rule as the write endpoints, which makes it a useful way to verify that a deployment's origin is configured correctly: a `403` means the origin is not on the form's allowlist, or the form is disabled.\n\n### `subscribe(request, options?)`\n\nSubmit a public subscription request.\n\nThe target ContentEdge form must be subscription-capable. If the form is enquiry-only, the API rejects the request.\n\n```typescript\ninterface SubscribeRequest {\n  email: string;\n  firstName?: string;\n  lastName?: string;\n  attributes?: Record<string, unknown>;\n  consent: boolean;\n  captchaToken?: string;\n  honeypot?: string;\n}\n```\n\nReturns:\n\n```typescript\ninterface PublicNewsletterResponse {\n  status: \"ACCEPTED\" | \"SUBSCRIBED\" | \"UNSUBSCRIBED\" | \"RECEIVED\";\n  message: string;\n}\n```\n\n### `submitEnquiry(request, options?)`\n\nSubmit a public enquiry request.\n\nThe target ContentEdge form must be enquiry-capable. If the form is subscription-only, the API rejects the request.\n\n```typescript\ninterface EnquiryRequest {\n  fields: Record<string, unknown>;\n  captchaToken?: string;\n  honeypot?: string;\n}\n```\n\nReturns `PublicNewsletterResponse`.\n\n### `confirm(token, options?)`\n\nConfirm a subscription using a token from a confirmation link.\n\n```typescript\nawait newsletter.confirm(\"confirmation-token\");\n```\n\nReturns `PublicNewsletterResponse`.\n\n### `unsubscribe(token, options?)`\n\nUnsubscribe using a token from an unsubscribe link.\n\n```typescript\nawait newsletter.unsubscribe(\"unsubscribe-token\");\n```\n\nReturns `PublicNewsletterResponse`.\n\n## Error Handling\n\nThe SDK throws `ContentEdgeNewsletterError` when the API rejects a request or returns an unexpected response shape.\n\n```typescript\nimport {\n  ContentEdgeNewsletterError,\n  createNewsletterClient,\n} from \"@codesocietyou/contentedge-newsletter-sdk\";\n\ntry {\n  await newsletter.subscribe({\n    email: \"reader@example.com\",\n    consent: true,\n  });\n} catch (error) {\n  if (error instanceof ContentEdgeNewsletterError) {\n    console.error(\"Newsletter request failed\", error.status, error.details);\n  } else {\n    console.error(\"Unknown error\", error);\n  }\n}\n```\n\nContentEdge returns two error body shapes and the SDK reads both: its own `{ status: \"ERROR\", message }` envelope for validation, security, and rate-limit failures, and Spring's `ProblemDetail` (`{ title, detail, status }`) when the tenant in the URL cannot be resolved. Either way you get a `ContentEdgeNewsletterError` whose `message` is the human-readable reason and whose `details` holds the parsed body.\n\nStatuses worth handling explicitly:\n\n- `400` — validation failed, or the tenant is unknown. For bean validation, `error.details.data` maps field names to messages.\n- `403` — the origin is not allowed, the form is disabled, the form does not support the operation you called, or captcha verification failed.\n- `429` — a public rate limit was exceeded.\n\nNetwork failures, aborts, and timeouts are **not** wrapped. They propagate as the underlying `TypeError` or `DOMException` so you can keep checking `error.name === \"AbortError\"` or `\"TimeoutError\"`.\n\n## Advanced Usage\n\n### Custom `fetch`\n\nYou can provide a custom `fetch` implementation for tests, SSR-compatible environments, or request instrumentation.\n\n```typescript\nconst newsletter = createNewsletterClient({\n  baseUrl: \"https://api.contentedgecms.com\",\n  tenant: \"example\",\n  formKey: \"homepage-newsletter\",\n  fetch: async (input, init) => {\n    console.debug(\"Newsletter request\", input);\n    return fetch(input, init);\n  },\n});\n```\n\n### Environment Configuration\n\nThe client is configured with a base URL and public form identifiers, so applications can switch environments without changing usage code.\n\n```typescript\nconst newsletter = createNewsletterClient({\n  baseUrl: import.meta.env.VITE_CONTENTEDGE_API_URL,\n  tenant: import.meta.env.VITE_CONTENTEDGE_TENANT,\n  formKey: \"homepage-newsletter\",\n});\n```\n\n### Timeouts and cancellation\n\nSet `timeoutMs` to bound every request made by a client:\n\n```typescript\nconst newsletter = createNewsletterClient({\n  baseUrl: \"https://api.contentedgecms.com\",\n  tenant: \"example\",\n  formKey: \"homepage-newsletter\",\n  timeoutMs: 10_000,\n});\n```\n\nPass a signal to cancel a single call, for example when a component unmounts:\n\n```typescript\nconst controller = new AbortController();\n\nconst pending = newsletter.subscribe(\n  { email: \"reader@example.com\", consent: true },\n  { signal: controller.signal }\n);\n\ncontroller.abort();\n```\n\nWhen both are present the request aborts on whichever fires first.\n\n### Browser origins and server-side usage\n\nContentEdge validates the browser `Origin` header against the public form's allowed origins on **every** public request, including `getFormConfig`. Allowed origins are required: a form with none configured rejects all public traffic, and ContentEdge disables such forms so the misconfiguration is visible to administrators instead of failing silently.\n\nBrowsers set `Origin` automatically. Server-side runtimes usually do not, so SSR, Node.js, integration tests, and backend-to-backend usage **must** supply a custom `fetch` that adds an allowed `Origin` header:\n\n```typescript\nconst newsletter = createNewsletterClient({\n  baseUrl: \"https://api.contentedgecms.com\",\n  tenant: \"example\",\n  formKey: \"homepage-newsletter\",\n  fetch: (input, init) =>\n    fetch(input, {\n      ...init,\n      headers: { ...init?.headers, Origin: \"https://example.com\" },\n    }),\n});\n```\n\nA request with a missing or unlisted origin fails with `403` and the message `Origin is not allowed for this form`.\n\n### Rate limits\n\nPublic endpoints are rate limited per IP, per email, and per form. Exceeding a limit returns `429` with the message `rate limit exceeded`. No `Retry-After` header is sent, so back off on your side rather than retrying immediately.\n\n## Public Form Usage\n\nApplications provide the configured tenant domain, `formKey`, and public form data. The same client API can be used from React, Vue, Svelte, static sites, or plain TypeScript applications.\n\nContentEdge public forms can be configured for three purposes:\n\n- **Subscription**: call `subscribe`.\n- **Enquiry**: call `submitEnquiry`.\n- **Subscription and enquiry**: call both methods with the same `tenant` and `formKey`.\n\nUse `getFormConfig` to discover which purpose a form has instead of assuming it.\n\nThe browser never sends admin-owned configuration. Target lists, templates, sender identities, enquiry recipients, and allowed origins stay in ContentEdge and are never returned to public clients. Public clients send only visitor-provided data such as email, attributes, enquiry fields, consent, captcha token, and honeypot value.\n\n### Subscriber lifecycle\n\nUnderstanding how ContentEdge treats a subscriber helps explain the responses you get:\n\n- With double opt-in enabled, `subscribe` creates a `PENDING_VERIFICATION` subscriber that is not yet a member of any contact list. Membership is granted only once the subscriber confirms.\n- `subscribe` always resolves with `ACCEPTED` regardless of whether the address is new, already subscribed, or suppressed. This is deliberate: it prevents the form from being used to test whether an address is on file.\n- Unsubscribing removes the subscriber from every contact list, so list counts always reflect people who can actually be emailed.\n- Addresses that hard bounced, complained, or were cleaned cannot be resubscribed through a public form.\n\n## Versioning\n\nThis project follows [Semantic Versioning](https://semver.org/). Breaking changes bump MAJOR.\n\n## License\n\nMIT\n\n## Support\n\n- Issues: https://github.com/ParapluOU/contentedge-newsletter-sdk/issues\n- Docs: https://github.com/ParapluOU/contentedge-newsletter-sdk#readme\n","readmeFilename":"README.md"}