# Email link delivery The survey form is reached via a personalized URL that carries the contact's CiviCRM ID + a server-issued checksum: ``` https://survey.fci.coop/?cid=&cs= ``` This document covers the three pieces needed to actually get those links into form-fillers' hands: 1. A **CiviCRM message template** that bakes the URL into an email body. 2. A way to **trigger sending** (one of three options below). 3. A **link-preview endpoint** in this app for ad-hoc / testing use. You don't need all three — most teams use #1 + the simplest version of #2. The preview endpoint (#3) is a debugging convenience. --- ## 1. Create the CiviCRM message template In CiviCRM: 1. Navigate to **Mailings → Message Templates → Add Message Template**. 2. Title: `Co-op Survey invitation` 3. Subject: ``` Time for your co-op survey ``` 4. Plain-text body (substitute your real domain): ``` Hello {contact.display_name}, Please take a few minutes to update us on your co-op's progress this month. The form pre-fills your prior responses — you only need to update what's changed. Open your survey: https://survey.fci.coop/?cid={contact.contact_id}&cs={contact.checksum} The link is personalized to you and expires in 14 days. If you no longer have it, reply to this email and we'll send a fresh one. With gratitude, The Food Co-op Initiative team ``` 5. HTML body (same content, slightly nicer): ```html

Hello {contact.display_name},

Please take a few minutes to update us on your co-op's progress this month. The form pre-fills your prior responses — you only need to update what's changed.

Open your survey

The link is personalized to you and expires in 14 days. If you no longer have it, reply to this email and we'll send a fresh one.

With gratitude,
The Food Co-op Initiative team

``` 6. Save. > **Token reference:** > - `{contact.display_name}` — the form-filler's name > - `{contact.contact_id}` — their CiviCRM ID (becomes `cid`) > - `{contact.checksum}` — a fresh secure token (becomes `cs`) > > All three are stock CiviCRM tokens. The checksum is generated at send time > and includes the recipient's contact-specific hash, so it's both > per-contact and per-send. --- ## 2. Trigger sending — three options ### Option A — Manual: stock CiviCRM Send Email (simplest) Best for: low volume, one-at-a-time, or you want to review each send. 1. Open the **organization's contact record** in CiviCRM. 2. **Relationships** tab → find the row with **"Primary Contact"** → click the related individual's name. 3. On the individual's contact view: **Actions → Send Email**. 4. **Use Template**: select `Co-op Survey invitation`. The subject and body populate; tokens render in the preview. 5. **Send**. CiviCRM logs the email as an Activity on the contact. Repeat per contact. Slow but bulletproof. ### Option B — Bulk: CiviMail (campaign-style send) Best for: send the survey invitation to every active co-op at once (e.g. monthly). 1. Build a Smart Group of contacts whose Primary Contact relationships point at orgs in active stages. Example query: - **Contacts** → **Advanced Search** - Add criteria: `Has relationship` → `Primary Contact` → status: Active - Optionally constrain by the related org's `Food_Co_op_Organizing.Stage` field - Save as Smart Group: "Active co-op primary contacts" 2. **Mailings → New Mailing**. 3. Recipients: the smart group above. 4. Choose template: `Co-op Survey invitation`. 5. Schedule send. Each recipient gets their own checksum embedded in the URL — CiviMail expands the tokens per-recipient. ### Option C — One-click: CiviRules action (most polished) Best for: a button-on-the-org-record workflow. Setup: 1. **Administer → CiviRules → Manage Rules → New Rule**. 2. Title: `Send Co-op Survey invitation`. 3. **Trigger**: pick a "manually trigger from org row" action if your CiviRules version supports it. Otherwise: trigger on org Stage change (auto-fires when staff advance an org). 4. **Action**: `Send email to contact via message template`. - Template: `Co-op Survey invitation`. - Recipient: **Contact in relationship** → "Primary Contact" → side B (the Individual). 5. Save. When staff change `Food_Co_op_Organizing.Stage` on an org, the rule fires and emails the org's Primary Contact automatically — no extra clicks. ### Combination You can use any combination. Common pattern: Option C for stage transitions (automatic) + Option A for ad-hoc resends. --- ## 3. The `/api/preview-link` endpoint (admin debugging) Generates a working URL for any contact, returning the URL and the contact/org context for verification. **Auth:** requires the `HEALTH_TOKEN` env var (same secret as `/api/health`). In development without `HEALTH_TOKEN` set, no auth required. **Request:** ```bash curl "https://survey.fci.coop/api/preview-link?cid=513&token=$HEALTH_TOKEN" ``` **Response:** ```json { "contactId": 513, "contactName": "Bob Sample", "orgId": 9609, "orgName": "A Sample Food Co-op", "orgStage": "Organizing", "url": "https://survey.fci.coop/?cid=513&cs=8d1f...", "checksum": "8d1f...", "ttlHours": 336 } ``` **Use cases:** - **Testing**: confirm a contact's setup before sending the real CiviCRM email. - **Manual / external sends**: copy the URL into Slack, ad-hoc email, an SMS, etc. - **Debugging**: if a contact reports the form not working, generate a fresh URL and try it yourself. - **Bulk export**: script-loop over a list of cids to produce a CSV of `(name, org, url)` for an external mail-merge. **Errors you may see:** - `404 — has no active Primary Contact relationship` → the contact isn't linked to any org. - `409 — multiple active relationships` → resolve in CiviCRM first; the form needs exactly one. - `501 — Stub mode active` → you forgot to set `CIVI_BASE_URL` etc. --- ## Operational runbook ### "We need to send invitations for this month" 1. (Bulk path) Use Option B: build/refresh the Smart Group, schedule the CiviMail mailing, send. 2. (Per-org path) Use Option A: open each org → Send Email to Primary Contact. 3. (Auto path) Use Option C: just advance stages; emails go out automatically. ### "Contact reports the link doesn't work" 1. Hit `/api/preview-link?cid=&token=`. 2. Verify the response shows the right org name + stage. 3. Send the URL from the response directly to the contact (or have them try it). 4. If `/api/preview-link` 404s, the contact lacks a Primary Contact relationship — fix in CiviCRM first. 5. If their old URL truly expired (older than 14 days / your `cs_offset` setting), CiviCRM rejects it. The new one from `preview-link` will work. ### "We want to invalidate all outstanding links for one contact" In CiviCRM, edit the contact's **hash** field (under their record → Edit). Bumping the hash invalidates every checksum issued for that contact. Use as a break-glass for compromised links.