{"info":{"_postman_id":"5f657eaa-049d-4189-ae68-03e0d3ec2e09","name":"OHPM Newsletter API","description":"<html><head></head><body><h1 id=\"only-hope-prison-ministries-self-hosted-newsletter-system\">Only Hope Prison Ministries — Self-Hosted Newsletter System</h1>\n<p>A serverless newsletter platform replacing Mailchimp, built for Only Hope Prison Ministries.</p>\n<h2 id=\"architecture\">Architecture</h2>\n<ul>\n<li><strong>AWS Lambda</strong> (Node.js 22.x) — All business logic runs as Lambda functions behind API Gateway.</li>\n<li><strong>Amazon API Gateway</strong> — REST API with proxy integration to Lambda.</li>\n<li><strong>Amazon SES</strong> — Transactional and bulk email sending (double opt-in confirmations, newsletter delivery, admin notifications).</li>\n<li><strong>Amazon S3</strong> — Image hosting for newsletter content (bucket: <code>ohpm-newsletters</code>).</li>\n<li><strong>PostgreSQL</strong> — Subscriber management, campaign tracking, bounce handling.</li>\n<li><strong>AWS SNS</strong> — Receives SES bounce/complaint notifications and forwards them to the <code>/bounce</code> webhook.</li>\n<li><strong>Deployed via AWS SAM / CloudFormation.</strong></li>\n</ul>\n<h2 id=\"cost\">Cost</h2>\n<p>Approximately <strong>$0.10 per 1,000 emails</strong> via SES, compared to Mailchimp's ~$20/month minimum. For a small ministry mailing list, this reduces newsletter costs to near-zero.</p>\n<h2 id=\"authentication\">Authentication</h2>\n<ul>\n<li><strong>Admin endpoints</strong> require an <code>adminPassword</code> field in the request body (POST) or query string (GET).</li>\n<li><strong>Public endpoints</strong> (subscribe, confirm, unsubscribe) require no authentication.</li>\n<li><strong>Tracking endpoints</strong> (open pixel, click redirect) require no authentication.</li>\n</ul>\n<h2 id=\"collection-variables\">Collection Variables</h2>\n<ul>\n<li><code>baseUrl</code> — The API Gateway base URL.</li>\n<li><code>adminPassword</code> — The admin password for protected endpoints (set this in your environment).</li>\n</ul>\n</body></html>","schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json","toc":[{"content":"Only Hope Prison Ministries — Self-Hosted Newsletter System","slug":"only-hope-prison-ministries-self-hosted-newsletter-system"}],"owner":"53534571","collectionId":"5f657eaa-049d-4189-ae68-03e0d3ec2e09","publishedId":"2sBXikoXEF","public":true,"customColor":{"top-bar":"FFFFFF","right-sidebar":"303030","highlight":"FF6C37"},"publishDate":"2026-03-26T22:30:25.000Z"},"item":[{"name":"Subscriber Management","item":[{"name":"Subscribe","id":"78a48cfd-5ec5-479a-b947-1eece8e9799d","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"email\": \"supporter@example.com\",\n\t\"firstName\": \"John\",\n\t\"lastName\": \"Doe\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/subscribe","description":"<p>Add a new email address to the newsletter subscriber list. Triggers a double opt-in confirmation email via Amazon SES. The subscriber is stored in PostgreSQL with <code>confirmed = false</code> until they click the confirmation link.</p>\n<p><strong>Request body:</strong></p>\n<ul>\n<li><code>email</code> (string, required) — The subscriber's email address.</li>\n<li><code>firstName</code> (string, optional) — First name for personalization.</li>\n<li><code>lastName</code> (string, optional) — Last name for personalization.</li>\n</ul>\n<p><strong>Behavior:</strong></p>\n<ul>\n<li>If the email is new, a confirmation token is generated and a confirmation email is sent.</li>\n<li>If the email already exists and is unconfirmed, a new confirmation email is re-sent.</li>\n<li>If the email already exists and is confirmed, a success message is returned without sending a duplicate email.</li>\n</ul>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"message\": \"Confirmation email sent. Please check your inbox.\"\n}\n</code></pre>\n","urlObject":{"path":["subscribe"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[],"variable":[]}},"response":[{"id":"85dab742-79cc-4159-9e88-944531005ac5","name":"Success — Confirmation email sent","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"email\": \"supporter@example.com\",\n\t\"firstName\": \"John\",\n\t\"lastName\": \"Doe\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/subscribe"},"status":"OK","code":200,"_postman_previewlanguage":"Text","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n\t\"message\": \"Confirmation email sent. Please check your inbox.\"\n}"}],"_postman_id":"78a48cfd-5ec5-479a-b947-1eece8e9799d"},{"name":"Confirm Subscription","id":"f5ac099e-96a1-4920-937b-b2c90085f9e9","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/confirm?token=abc123-example-token","description":"<p>Confirm a subscriber's email address via the double opt-in token. This endpoint is hit when the subscriber clicks the confirmation link in their email.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>token</code> (string, required) — The unique confirmation token generated during subscribe.</li>\n</ul>\n<p><strong>Behavior:</strong></p>\n<ul>\n<li>Sets <code>confirmed = true</code> and records <code>confirmed_at</code> timestamp in the database.</li>\n<li>On success, <strong>redirects (302)</strong> the subscriber to the OHPM website with a success message.</li>\n<li>If the token is invalid or expired, redirects to an error page.</li>\n</ul>\n<p>This is a public endpoint — no authentication required.</p>\n","urlObject":{"path":["confirm"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>The double opt-in confirmation token sent to the subscriber's email.</p>\n","type":"text/plain"},"key":"token","value":"abc123-example-token"}],"variable":[]}},"response":[],"_postman_id":"f5ac099e-96a1-4920-937b-b2c90085f9e9"},{"name":"Unsubscribe","id":"b61891d7-3e27-4812-a117-a944923080d1","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/unsubscribe?token=subscriber-unsubscribe-token&cid=campaign-uuid","description":"<p>Unsubscribe a subscriber from the newsletter. Performs a soft delete — sets <code>confirmed = false</code> and records <code>unsubscribed_at</code> timestamp. Also tracks which campaign caused the unsubscribe for analytics.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>token</code> (string, required) — The subscriber's unique unsubscribe token, embedded in every newsletter email.</li>\n<li><code>cid</code> (string, required) — The campaign ID of the newsletter that contained this unsubscribe link.</li>\n</ul>\n<p><strong>Behavior:</strong></p>\n<ul>\n<li>The subscriber record is preserved (soft delete) but will no longer receive newsletters.</li>\n<li>The unsubscribe event is recorded against the campaign for reporting.</li>\n<li>On success, <strong>redirects (302)</strong> to the OHPM website with a confirmation message.</li>\n</ul>\n<p>This is a public endpoint — no authentication required. Every newsletter email includes a unique unsubscribe link per subscriber.</p>\n","urlObject":{"path":["unsubscribe"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>The subscriber's unique unsubscribe token (different from the opt-in confirmation token).</p>\n","type":"text/plain"},"key":"token","value":"subscriber-unsubscribe-token"},{"description":{"content":"<p>The campaign ID that contained the unsubscribe link. Used to track which newsletter caused the unsubscribe.</p>\n","type":"text/plain"},"key":"cid","value":"campaign-uuid"}],"variable":[]}},"response":[],"_postman_id":"b61891d7-3e27-4812-a117-a944923080d1"}],"id":"32a20493-fac3-40fc-9021-9d9ad28f89a4","description":"<p>Endpoints for managing the subscriber list. Subscribers go through a double opt-in flow: they submit their email via <code>/subscribe</code>, receive a confirmation email from SES, and must click the link to hit <code>/confirm</code> before they are added to the active list. Unsubscribe links in every newsletter point to <code>/unsubscribe</code>, which soft-deletes the subscriber and tracks which campaign caused the unsubscribe.</p>\n","_postman_id":"32a20493-fac3-40fc-9021-9d9ad28f89a4"},{"name":"Newsletter Publishing","item":[{"name":"Send Newsletter","id":"7491aeab-df81-41d6-8b9b-279f9a7f0109","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"adminPassword\": \"\",\n\t\"slug\": \"march-2026-update\",\n\t\"subject\": \"March 2026 — Ministry Update from Only Hope\",\n\t\"title\": \"March 2026 Ministry Update\",\n\t\"htmlContent\": \"<h1>March 2026 Update</h1><p>Dear friends, here is what God has been doing...</p>\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/send-newsletter","description":"<p>Send a newsletter email to all confirmed subscribers. This is the core sending endpoint.</p>\n<p><strong>Request body:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n<li><code>slug</code> (string, required) — URL-friendly identifier for the campaign (e.g., <code>march-2026-update</code>). Used in tracking URLs and the published web version.</li>\n<li><code>subject</code> (string, required) — The email subject line.</li>\n<li><code>title</code> (string, required) — The newsletter title (used in metadata and headers).</li>\n<li><code>htmlContent</code> (string, required) — The full HTML content of the newsletter.</li>\n</ul>\n<p><strong>What happens server-side:</strong></p>\n<ol>\n<li>A new campaign record is created in PostgreSQL.</li>\n<li>For each confirmed subscriber, the Lambda:<ul>\n<li>Injects a unique 1x1 tracking pixel (<code>/track/open?cid=...&amp;sid=...</code>) before the closing <code>&lt;/body&gt;</code> tag.</li>\n<li>Rewrites every <code>&lt;a href=\"...\"&gt;</code> link to route through <code>/track/click?cid=...&amp;sid=...&amp;url=...</code> for click tracking.</li>\n<li>Injects a unique unsubscribe link.</li>\n</ul>\n</li>\n<li>Emails are batch-sent via Amazon SES (respecting SES rate limits).</li>\n<li>The campaign's <code>sent_count</code> is recorded.</li>\n</ol>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"message\": \"Newsletter sent successfully\",\n    \"campaignId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n    \"sentCount\": 247\n}\n</code></pre>\n<p><strong>Cost:</strong> ~$0.10 per 1,000 emails via SES (vs. ~$20/month on Mailchimp).</p>\n","urlObject":{"path":["send-newsletter"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[],"variable":[]}},"response":[{"id":"fc9cea9e-a036-45fe-89ad-f1c2a2464b89","name":"Success — Newsletter sent","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"adminPassword\": \"\",\n\t\"slug\": \"march-2026-update\",\n\t\"subject\": \"March 2026 — Ministry Update from Only Hope\",\n\t\"title\": \"March 2026 Ministry Update\",\n\t\"htmlContent\": \"<h1>March 2026 Update</h1><p>Dear friends, here is what God has been doing...</p>\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/send-newsletter"},"status":"OK","code":200,"_postman_previewlanguage":"Text","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n\t\"message\": \"Newsletter sent successfully\",\n\t\"campaignId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n\t\"sentCount\": 247\n}"}],"_postman_id":"7491aeab-df81-41d6-8b9b-279f9a7f0109"},{"name":"Publish Newsletter to Website","id":"582f75ed-7bba-4b34-b48c-5fe319dd9aae","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"adminPassword\": \"\",\n\t\"slug\": \"march-2026-update\",\n\t\"title\": \"March 2026 Ministry Update\",\n\t\"date\": \"2026-03-26\",\n\t\"imageUrl\": \"https://ohpm-newsletters.s3.us-west-1.amazonaws.com/images/march-2026-header.jpg\",\n\t\"htmlContent\": \"<h1>March 2026 Update</h1><p>Dear friends, here is what God has been doing...</p>\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/publish-newsletter","description":"<p>Publish a newsletter's HTML to the OHPM website hosted on SiteGround, and update the <code>ministry-newsletters.json</code> index file so the newsletter appears on the website's newsletter archive page.</p>\n<p><strong>Request body:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n<li><code>slug</code> (string, required) — URL-friendly identifier. The HTML file will be saved as <code>{slug}.html</code> on the web server.</li>\n<li><code>title</code> (string, required) — The newsletter title for the JSON index.</li>\n<li><code>date</code> (string, required) — Publication date in <code>YYYY-MM-DD</code> format.</li>\n<li><code>imageUrl</code> (string, optional) — Featured image URL for the newsletter tile/card on the archive page.</li>\n<li><code>htmlContent</code> (string, required) — The full HTML content of the newsletter.</li>\n</ul>\n<p><strong>What happens server-side:</strong></p>\n<ol>\n<li>The Lambda SSHs into SiteGround hosting using stored credentials.</li>\n<li>Uploads <code>{slug}.html</code> to the newsletters directory on the web server.</li>\n<li>Downloads the existing <code>ministry-newsletters.json</code>, prepends the new newsletter entry, and re-uploads the updated JSON.</li>\n</ol>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"message\": \"Newsletter published successfully\",\n    \"url\": \"https://onlyhopeprisonministries.org/newsletters/march-2026-update.html\"\n}\n</code></pre>\n","urlObject":{"path":["publish-newsletter"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[],"variable":[]}},"response":[{"id":"8a1c4e46-a917-4592-a822-ba050685bb00","name":"Success — Published to website","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"adminPassword\": \"\",\n\t\"slug\": \"march-2026-update\",\n\t\"title\": \"March 2026 Ministry Update\",\n\t\"date\": \"2026-03-26\",\n\t\"imageUrl\": \"https://ohpm-newsletters.s3.us-west-1.amazonaws.com/images/march-2026-header.jpg\",\n\t\"htmlContent\": \"<h1>March 2026 Update</h1><p>Dear friends...</p>\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/publish-newsletter"},"status":"OK","code":200,"_postman_previewlanguage":"Text","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n\t\"message\": \"Newsletter published successfully\",\n\t\"url\": \"https://onlyhopeprisonministries.org/newsletters/march-2026-update.html\"\n}"}],"_postman_id":"582f75ed-7bba-4b34-b48c-5fe319dd9aae"},{"name":"Upload Image","id":"250f37f1-6e38-4cb0-9160-f344daa18ac4","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"adminPassword\": \"\",\n\t\"fileName\": \"march-2026-header.jpg\",\n\t\"fileData\": \"<base64-encoded-image-data>\",\n\t\"contentType\": \"image/jpeg\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/upload-image","description":"<p>Upload an image to the S3 bucket (<code>ohpm-newsletters/images/</code>) for use in newsletter content. Returns the public S3 URL that can be referenced in the newsletter HTML.</p>\n<p><strong>Request body:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n<li><code>fileName</code> (string, required) — The desired filename for the image (e.g., <code>march-2026-header.jpg</code>).</li>\n<li><code>fileData</code> (string, required) — The image file contents as a Base64-encoded string.</li>\n<li><code>contentType</code> (string, required) — The MIME type of the image (e.g., <code>image/jpeg</code>, <code>image/png</code>, <code>image/webp</code>).</li>\n</ul>\n<p><strong>Behavior:</strong></p>\n<ul>\n<li>The image is uploaded to <code>s3://ohpm-newsletters/images/{fileName}</code>.</li>\n<li>The S3 object is set to public-read so it can be embedded in emails and web pages.</li>\n</ul>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"message\": \"Image uploaded successfully\",\n    \"url\": \"https://ohpm-newsletters.s3.us-west-1.amazonaws.com/images/march-2026-header.jpg\"\n}\n</code></pre>\n<p><strong>Tip:</strong> Upload images first, then reference their S3 URLs in the <code>htmlContent</code> field of <code>/send-newsletter</code> and <code>/publish-newsletter</code>.</p>\n","urlObject":{"path":["upload-image"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[],"variable":[]}},"response":[{"id":"5552c1d3-b1fb-4952-9f35-4f79c3f8b7fd","name":"Success — Image uploaded","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n\t\"adminPassword\": \"\",\n\t\"fileName\": \"march-2026-header.jpg\",\n\t\"fileData\": \"<base64-encoded-image-data>\",\n\t\"contentType\": \"image/jpeg\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/upload-image"},"status":"OK","code":200,"_postman_previewlanguage":"Text","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n\t\"message\": \"Image uploaded successfully\",\n\t\"url\": \"https://ohpm-newsletters.s3.us-west-1.amazonaws.com/images/march-2026-header.jpg\"\n}"}],"_postman_id":"250f37f1-6e38-4cb0-9160-f344daa18ac4"}],"id":"b7a8cb72-ee61-4e6e-a8a3-2eeceae224a8","description":"<p>Admin endpoints for composing and sending newsletters. The workflow is:</p>\n<ol>\n<li>Upload any images via <code>/upload-image</code> to get S3 URLs.</li>\n<li>Send the newsletter to all confirmed subscribers via <code>/send-newsletter</code> (injects tracking pixels and rewrites links for click tracking).</li>\n<li>Publish the newsletter HTML to the OHPM website via <code>/publish-newsletter</code> (SSHs into SiteGround hosting).</li>\n</ol>\n<p>All endpoints in this folder require <code>adminPassword</code> in the request body.</p>\n","_postman_id":"b7a8cb72-ee61-4e6e-a8a3-2eeceae224a8"},{"name":"Tracking & Analytics","item":[{"name":"Track Open (Pixel)","id":"3fa5bfc6-9ed7-4e86-8788-f1c3ccb7f758","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/track/open?cid=campaign-uuid&sid=subscriber-uuid","description":"<p>Returns a 1x1 transparent PNG tracking pixel. When an email client loads this image, an \"open\" event is recorded in the database.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>cid</code> (string, required) — The campaign ID.</li>\n<li><code>sid</code> (string, required) — The subscriber ID.</li>\n</ul>\n<p><strong>Behavior:</strong></p>\n<ul>\n<li>Records the open event with a timestamp in PostgreSQL.</li>\n<li><strong>Deduplication:</strong> Only the first open per subscriber per campaign is recorded. Subsequent loads of the same pixel are served but not counted again.</li>\n<li>Returns a 1x1 transparent PNG image with <code>Content-Type: image/png</code> and <code>Cache-Control: no-store</code> to prevent caching.</li>\n</ul>\n<p><strong>Note:</strong> Open rates are inflated by Apple Mail Privacy Protection (iOS 15+), which pre-fetches all images. Use click tracking for more accurate engagement data.</p>\n<p>This endpoint requires no authentication — it is embedded as an <code>&lt;img&gt;</code> tag in the newsletter HTML.</p>\n","urlObject":{"path":["track","open"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>The campaign ID (UUID) for the newsletter that was sent.</p>\n","type":"text/plain"},"key":"cid","value":"campaign-uuid"},{"description":{"content":"<p>The subscriber ID (UUID) who received the email.</p>\n","type":"text/plain"},"key":"sid","value":"subscriber-uuid"}],"variable":[]}},"response":[],"_postman_id":"3fa5bfc6-9ed7-4e86-8788-f1c3ccb7f758"},{"name":"Track Click (Redirect)","id":"a422aeb6-a5e0-4795-a8d8-92ffae96ae40","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/track/click?cid=campaign-uuid&sid=subscriber-uuid&url=https%3A%2F%2Fonlyhopeprisonministries.org%2Fdonate","description":"<p>Records a click event and redirects the subscriber to the actual destination URL via a 302 redirect.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>cid</code> (string, required) — The campaign ID.</li>\n<li><code>sid</code> (string, required) — The subscriber ID.</li>\n<li><code>url</code> (string, required) — The actual destination URL (URL-encoded).</li>\n</ul>\n<p><strong>Behavior:</strong></p>\n<ul>\n<li>Records the click event (campaign, subscriber, URL, timestamp) in PostgreSQL.</li>\n<li>Every click is recorded (not deduped like opens) to capture full engagement data.</li>\n<li>Returns a <strong>302 redirect</strong> to the decoded <code>url</code> parameter.</li>\n</ul>\n<p><strong>Why this is the most accurate metric:</strong> Unlike open tracking, click tracking is not affected by Apple Mail Privacy Protection. A click event means the subscriber genuinely engaged with the content.</p>\n<p>This endpoint requires no authentication — all links in the sent newsletter are rewritten to route through this endpoint.</p>\n","urlObject":{"path":["track","click"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>The campaign ID (UUID) for the newsletter.</p>\n","type":"text/plain"},"key":"cid","value":"campaign-uuid"},{"description":{"content":"<p>The subscriber ID (UUID) who clicked the link.</p>\n","type":"text/plain"},"key":"sid","value":"subscriber-uuid"},{"description":{"content":"<p>The actual destination URL (URL-encoded). The subscriber is redirected here after the click is recorded.</p>\n","type":"text/plain"},"key":"url","value":"https%3A%2F%2Fonlyhopeprisonministries.org%2Fdonate"}],"variable":[]}},"response":[],"_postman_id":"a422aeb6-a5e0-4795-a8d8-92ffae96ae40"}],"id":"6abbf0ee-fa1f-43ee-8a00-22202efe3b22","description":"<p>Invisible tracking endpoints embedded in every sent newsletter. These are not called manually — they are injected into newsletter HTML by the <code>/send-newsletter</code> endpoint.</p>\n<ul>\n<li><strong>Open tracking:</strong> A 1x1 transparent pixel image is appended to every email. When the subscriber's email client loads the image, an open event is recorded.</li>\n<li><strong>Click tracking:</strong> Every link in the newsletter is rewritten to pass through <code>/track/click</code>, which records the click and then 302-redirects to the actual destination URL.</li>\n</ul>\n<p><strong>Note on Apple Mail Privacy Protection:</strong> Since iOS 15, Apple Mail pre-fetches all images (including tracking pixels), which inflates open rates. Click tracking is unaffected and is the most accurate engagement metric.</p>\n","_postman_id":"6abbf0ee-fa1f-43ee-8a00-22202efe3b22"},{"name":"Campaign Reports","item":[{"name":"Campaign Stats (Overview)","id":"0f7fe332-174c-40ac-b56e-3c41e90eb464","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/stats?adminPassword=","description":"<p>Retrieve a list of all campaigns with per-campaign performance metrics.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n</ul>\n<p><strong>Response includes per campaign:</strong></p>\n<ul>\n<li><code>id</code> — Campaign UUID.</li>\n<li><code>slug</code> — URL-friendly campaign identifier.</li>\n<li><code>subject</code> — Email subject line.</li>\n<li><code>sentAt</code> — Timestamp when the campaign was sent.</li>\n<li><code>sentCount</code> — Number of emails sent.</li>\n<li><code>opens</code> — Number of unique opens.</li>\n<li><code>openRate</code> — Opens / sent as a percentage.</li>\n<li><code>clicks</code> — Total click events.</li>\n<li><code>bounces</code> — Number of bounced emails.</li>\n<li><code>unsubscribes</code> — Number of unsubscribes triggered by this campaign.</li>\n</ul>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"campaigns\": [\n        {\n            \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n            \"slug\": \"march-2026-update\",\n            \"subject\": \"March 2026 — Ministry Update\",\n            \"sentAt\": \"2026-03-26T14:00:00Z\",\n            \"sentCount\": 247,\n            \"opens\": 183,\n            \"openRate\": 74.1,\n            \"clicks\": 62,\n            \"bounces\": 2,\n            \"unsubscribes\": 1\n        }\n    ]\n}\n</code></pre>\n","urlObject":{"path":["stats"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>Admin password for authentication.</p>\n","type":"text/plain"},"key":"adminPassword","value":""}],"variable":[]}},"response":[{"id":"f69e25f9-53ef-495a-801a-07593efe3464","name":"Success — Campaign list with metrics","originalRequest":{"method":"GET","header":[],"url":{"raw":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/stats?adminPassword=","host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"path":["stats"],"query":[{"key":"adminPassword","value":""}]}},"status":"OK","code":200,"_postman_previewlanguage":"Text","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n\t\"campaigns\": [\n\t\t{\n\t\t\t\"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n\t\t\t\"slug\": \"march-2026-update\",\n\t\t\t\"subject\": \"March 2026 — Ministry Update\",\n\t\t\t\"sentAt\": \"2026-03-26T14:00:00Z\",\n\t\t\t\"sentCount\": 247,\n\t\t\t\"opens\": 183,\n\t\t\t\"openRate\": 74.1,\n\t\t\t\"clicks\": 62,\n\t\t\t\"bounces\": 2,\n\t\t\t\"unsubscribes\": 1\n\t\t}\n\t]\n}"}],"_postman_id":"0f7fe332-174c-40ac-b56e-3c41e90eb464"},{"name":"Campaign Detail","id":"fcf1d1b0-9bb2-49a5-b248-60caaadf57a7","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/stats?adminPassword=&campaign=a1b2c3d4-e5f6-7890-abcd-ef1234567890","description":"<p>Retrieve detailed metrics for a specific campaign, including top clicked links and recent engagement activity.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n<li><code>campaign</code> (string, required) — The campaign UUID.</li>\n</ul>\n<p><strong>Response includes:</strong></p>\n<ul>\n<li>All overview metrics (sent, opens, open rate, clicks, bounces, unsubscribes).</li>\n<li><code>topLinks</code> — Ranked list of most-clicked URLs in the newsletter.</li>\n<li><code>recentOpens</code> — Most recent open events with subscriber info and timestamps.</li>\n<li><code>recentClicks</code> — Most recent click events with subscriber info, URL clicked, and timestamps.</li>\n</ul>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"campaign\": {\n        \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n        \"slug\": \"march-2026-update\",\n        \"subject\": \"March 2026 — Ministry Update\",\n        \"sentCount\": 247,\n        \"opens\": 183,\n        \"clicks\": 62\n    },\n    \"topLinks\": [\n        { \"url\": \"https://onlyhopeprisonministries.org/donate\", \"clicks\": 34 },\n        { \"url\": \"https://onlyhopeprisonministries.org/events\", \"clicks\": 18 }\n    ],\n    \"recentOpens\": [\n        { \"email\": \"supporter@example.com\", \"openedAt\": \"2026-03-26T15:12:00Z\" }\n    ],\n    \"recentClicks\": [\n        { \"email\": \"supporter@example.com\", \"url\": \"https://onlyhopeprisonministries.org/donate\", \"clickedAt\": \"2026-03-26T15:13:00Z\" }\n    ]\n}\n</code></pre>\n","urlObject":{"path":["stats"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>Admin password for authentication.</p>\n","type":"text/plain"},"key":"adminPassword","value":""},{"description":{"content":"<p>Campaign UUID to retrieve detailed metrics for.</p>\n","type":"text/plain"},"key":"campaign","value":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}],"variable":[]}},"response":[],"_postman_id":"fcf1d1b0-9bb2-49a5-b248-60caaadf57a7"},{"name":"Subscriber Engagement History","id":"f30844e9-94cb-46b8-beed-29976d64ed8a","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/stats?adminPassword=&subscriber=subscriber-uuid","description":"<p>Retrieve the full engagement history for a specific subscriber across all campaigns.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n<li><code>subscriber</code> (string, required) — The subscriber UUID.</li>\n</ul>\n<p><strong>Response includes:</strong></p>\n<ul>\n<li>Subscriber info (email, name, confirmed status, subscription date).</li>\n<li>Per-campaign engagement: which newsletters they opened, which links they clicked, and timestamps.</li>\n</ul>\n<p>Useful for understanding individual supporter engagement before outreach.</p>\n","urlObject":{"path":["stats"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>Admin password for authentication.</p>\n","type":"text/plain"},"key":"adminPassword","value":""},{"description":{"content":"<p>Subscriber UUID to retrieve engagement history for.</p>\n","type":"text/plain"},"key":"subscriber","value":"subscriber-uuid"}],"variable":[]}},"response":[],"_postman_id":"f30844e9-94cb-46b8-beed-29976d64ed8a"},{"name":"List Growth Data","id":"a419451a-7583-4c19-9520-5992ec6b4157","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/stats?adminPassword=&growth=true","description":"<p>Retrieve subscriber list growth data over time, suitable for rendering a growth chart.</p>\n<p><strong>Query parameters:</strong></p>\n<ul>\n<li><code>adminPassword</code> (string, required) — Admin authentication.</li>\n<li><code>growth</code> (string, required) — Must be <code>true</code>.</li>\n</ul>\n<p><strong>Response includes:</strong></p>\n<ul>\n<li>Time-series data showing confirmed subscriber count over time.</li>\n<li>New subscriptions, unsubscribes, and net growth per period.</li>\n</ul>\n<p><strong>Example response:</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"growth\": [\n        { \"date\": \"2026-01-01\", \"totalSubscribers\": 210, \"newSubscribers\": 12, \"unsubscribes\": 1 },\n        { \"date\": \"2026-02-01\", \"totalSubscribers\": 228, \"newSubscribers\": 19, \"unsubscribes\": 1 },\n        { \"date\": \"2026-03-01\", \"totalSubscribers\": 247, \"newSubscribers\": 21, \"unsubscribes\": 2 }\n    ]\n}\n</code></pre>\n","urlObject":{"path":["stats"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[{"description":{"content":"<p>Admin password for authentication.</p>\n","type":"text/plain"},"key":"adminPassword","value":""},{"description":{"content":"<p>Set to 'true' to retrieve subscriber list growth data over time.</p>\n","type":"text/plain"},"key":"growth","value":"true"}],"variable":[]}},"response":[],"_postman_id":"a419451a-7583-4c19-9520-5992ec6b4157"}],"id":"f565f665-7e71-4ed9-8dfe-682ec4469c12","description":"<p>Admin reporting endpoint for viewing campaign performance metrics. Provides aggregate stats, per-campaign detail views, individual subscriber engagement history, and list growth data.</p>\n<p>Requires <code>adminPassword</code> as a query parameter.</p>\n","_postman_id":"f565f665-7e71-4ed9-8dfe-682ec4469c12"},{"name":"Bounce Handling","item":[{"name":"Process Bounce/Complaint (SNS Webhook)","id":"54b7da34-ddb2-4fd4-a614-21a04329c2ef","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"x-amz-sns-message-type","value":"Notification","description":"<p>SNS message type header. Will be 'SubscriptionConfirmation' on initial setup or 'Notification' for bounce/complaint events.</p>\n"}],"body":{"mode":"raw","raw":"{\n\t\"Type\": \"Notification\",\n\t\"MessageId\": \"example-message-id\",\n\t\"TopicArn\": \"arn:aws:sns:us-west-1:123456789:ses-bounces\",\n\t\"Message\": \"{\\\"notificationType\\\":\\\"Bounce\\\",\\\"bounce\\\":{\\\"bounceType\\\":\\\"Permanent\\\",\\\"bouncedRecipients\\\":[{\\\"emailAddress\\\":\\\"invalid@example.com\\\"}]},\\\"mail\\\":{\\\"source\\\":\\\"newsletters@onlyhopeprisonministries.org\\\"}}\",\n\t\"Timestamp\": \"2026-03-26T14:30:00Z\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/bounce","description":"<p>Receives AWS SNS notifications triggered by Amazon SES bounce and complaint events. This endpoint is registered as an SNS HTTP/HTTPS subscription.</p>\n<p><strong>SNS Message Types handled:</strong></p>\n<ol>\n<li><p><strong>SubscriptionConfirmation</strong> — Automatically confirms the SNS subscription (one-time setup).</p>\n</li>\n<li><p><strong>Notification (Bounce):</strong></p>\n<ul>\n<li><strong>Hard bounce</strong> (Permanent) — The email address does not exist or permanently rejects mail. The subscriber is <strong>immediately disabled</strong> (<code>confirmed = false</code>, <code>disabled_reason = 'hard_bounce'</code>). An admin notification email is sent.</li>\n<li><strong>Soft bounce</strong> (Transient) — Temporary delivery failure (mailbox full, server down, etc.). A <code>soft_bounce_count</code> counter is incremented. After <strong>3 soft bounces</strong>, the subscriber is disabled. No admin notification unless the threshold is reached.</li>\n</ul>\n</li>\n<li><p><strong>Notification (Complaint):</strong></p>\n<ul>\n<li>The subscriber marked the email as spam in their email client. The subscriber is <strong>immediately disabled</strong> (<code>confirmed = false</code>, <code>disabled_reason = 'complaint'</code>). An admin notification email is sent — complaints are serious and can affect SES sending reputation.</li>\n</ul>\n</li>\n</ol>\n<p><strong>Admin notifications:</strong> On every hard bounce and complaint, an email is sent to the admin address summarizing the event (which subscriber, which campaign, bounce type, etc.).</p>\n<p><strong>Example SNS Notification payload (hard bounce):</strong></p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n    \"Type\": \"Notification\",\n    \"Message\": \"{\\\"notificationType\\\":\\\"Bounce\\\",\\\"bounce\\\":{\\\"bounceType\\\":\\\"Permanent\\\",\\\"bouncedRecipients\\\":[{\\\"emailAddress\\\":\\\"invalid@example.com\\\"}]}}\"\n}\n</code></pre>\n<p><strong>Note:</strong> The <code>Message</code> field is a JSON string that must be parsed. This is standard SNS behavior — the outer envelope is SNS metadata, and the inner <code>Message</code> contains the SES notification JSON.</p>\n<p>This endpoint requires no authentication — it is secured by SNS message signature verification.</p>\n","urlObject":{"path":["bounce"],"host":["https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"],"query":[],"variable":[]}},"response":[{"id":"f6182a65-cfc3-4f1f-b256-f87ab6bf71ce","name":"Success — Bounce processed","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"x-amz-sns-message-type","value":"Notification"}],"body":{"mode":"raw","raw":"{\n\t\"Type\": \"Notification\",\n\t\"MessageId\": \"example-message-id\",\n\t\"TopicArn\": \"arn:aws:sns:us-west-1:123456789:ses-bounces\",\n\t\"Message\": \"{\\\"notificationType\\\":\\\"Bounce\\\",\\\"bounce\\\":{\\\"bounceType\\\":\\\"Permanent\\\",\\\"bouncedRecipients\\\":[{\\\"emailAddress\\\":\\\"invalid@example.com\\\"}]},\\\"mail\\\":{\\\"source\\\":\\\"newsletters@onlyhopeprisonministries.org\\\"}}\",\n\t\"Timestamp\": \"2026-03-26T14:30:00Z\"\n}"},"url":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod/bounce"},"status":"OK","code":200,"_postman_previewlanguage":"Text","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n\t\"message\": \"Bounce processed successfully\"\n}"}],"_postman_id":"54b7da34-ddb2-4fd4-a614-21a04329c2ef"}],"id":"f3a86a59-9cc2-40e7-8331-7ec60b5141f2","description":"<p>SNS webhook endpoint that receives bounce and complaint notifications from Amazon SES. This endpoint is not called manually — it is configured as an SNS subscription target in AWS.</p>\n<p>When SES encounters a bounce or complaint, SNS delivers a notification to this endpoint, which processes it and updates the subscriber's status accordingly.</p>\n","_postman_id":"f3a86a59-9cc2-40e7-8331-7ec60b5141f2"}],"variable":[{"key":"baseUrl","value":"https://jt2zjj4lkg.execute-api.us-west-1.amazonaws.com/prod"},{"key":"adminPassword","value":""}]}