# Email link delivery The check-in form is reached via a personalized URL that carries the contact's CiviCRM ID + a server-issued checksum: ``` https://check-in.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 Check-in invitation` 3. Subject: ``` Time for your co-op check-in ``` 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 check-in: https://check-in.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 check-in

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 Check-in 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 check-in 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 Check-in 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 Check-in 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 Check-in 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://check-in.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://check-in.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.