Guides
People
Everyone who signed up: where they came from, what they answered, and the emails you send them.
Use your agent. Paste this into it:
Read hunchwire.com/docs/people and tell me who signed up this week.
People
A person is anyone who signed up to a project. List them, save views of them, export them, and change their place or state.
GET
/projects/:id/people
List everyone who submitted a form, one row per person, newest first.
Show 9 details
- Takes filters and
days, like Analytics. Withoutdays, it lists the last 30 days.days=alllists everyone. - A person is listed when they submitted in the period. With filters, only submissions from the browsers the filters pick count.
page=2returns the second 50. The founder's screen shows 50 a page. Withoutpage, you get everyone.?q=adakeeps only emails containingada. A number matches however it's typed:?q=416 555finds+14165550123.- It lists everyone except removed people, like the founder's All view. To narrow it like their segment switch, send one of these:
state=waiting,state=invitedorstate=removed.segment=top_referrers: people who brought at least one friend, most friends first.segment=<id>: a saved segment (see Segments). It applies its conditions when you send none of your own.
ids[]=lists only those people.filter[answer][]=<question key>:<answer>(such asq_0qjfp5jp:Notion) picks people by what they answered after signing up.exclude[answer][]=leaves them out. It works on Analytics too, where it picks visitors from the browsers they signed up in.GET /projects/:id/follow_uplists the question keys.- Returns one row per person:
id: their first submission ever. It's the same in every period. Use it to open their profile.email: the address they first signed up with, exactly.phone: their mobile number, for someone who signed up by number. The other isnull.forms: the forms they submitted.signed_up_at: the time of their first submission in the period.signed_up_on: its page.page_url: its full URL.state:waiting,invitedorremoved.invitedmeans an invite exists for the exact email they signed up with.removedmeans the founder or an agent took them off the list. Removed people keep their data and still count in Analytics.removed_at: when they were removed, ornull.language: their browser's language when they signed up, such asenorfr-CA.city: where they signed up, from Cloudflare.languageandcityarenullwhen not known. Hunchwire never keeps the IP.channel,sourceandcampaign: where they first came from.campaignis the campaign link, ornullwhen untagged.visitor_ids: the browsers they signed up from. Open one withGET /projects/:id/analytics/visitors/:visitor_idto see what they read and clicked before signing up.clicked: the tracked clicks from their browser, in order. Use it before an interview, to see which plan they wanted.place: their place in the beta list. 1 is first. It'snullonce they're removed, and everyone behind them moves up.moved: places they were moved by hand. Positive is up. Most people have 0.friends: how many friends count for them: people who joined through their link, or were let in by it while waiting, once both have confirmed.referred_by: the email or number of whoever brought them, ornull.unlocked: true once they've brought enough friends to get access. It applies only when sharers get access.answers: what they answered after signing up (see Questions after sign-up). Each haskey,question(its wording when they answered),answerandanswered_at. It's empty when they answered nothing.invite: their newest invite into the founder's product (see Let people in), ornull.confirmed_at: when they confirmed their email, ornull. It's the earliest across their submissions.consent_text,consent_versionandconsent_sender: proof of consent (CASL). They hold the exact wording shown, its version, and who it named. They come from whichever of the person's submissions showed a consent line, and arenullwhen none did.first_name: the first name a sign-up sent, ornull.list: the list they joined, ornull.via: how they first signed up:embed(the form) orapi.joined_product_at: when the founder's product reported they joined it (see API), ornull.
- A person is an email with any
+tagdropped, and for Gmail, its dots too. Soada+1@gmail.comshares a place withada@gmail.com.
curl https://app.hunchwire.com/projects/:id/people \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/people",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
Segments
A segment is a saved People view, such as "Said Notion" or "From Reddit". The founder or you can pick the same people again to export, invite or email them. A segment saves who, never when: it keeps no period.
GET
/projects/:id/segments
List every saved segment, by name.
Details
- Each has
id,name,query,created_atandupdated_at. query: its conditions, fromstate,segment,q,filterandexclude.
curl https://app.hunchwire.com/projects/:id/segments \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/segments",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/segments", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
POST
/projects/:id/segments
Save a segment.
Details
- A name is 1 to 40 characters, and unique in the project.
- Returns 422 for a blank or used name, a query with no condition, an unknown state or filter, or an email address or phone number anywhere in it.
curl -X POST https://app.hunchwire.com/projects/:id/segments \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Said Notion", "query": {"filter": {"answer": ["q_0qjfp5jp:Notion"]}}}'
import os, requests
r = requests.post(
"https://app.hunchwire.com/projects/:id/segments",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"name": "Said Notion", "query": {"filter": {"answer": ["q_0qjfp5jp:Notion"]}}},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/segments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"name": "Said Notion", "query": {"filter": {"answer": ["q_0qjfp5jp:Notion"]}}}),
})
const data = await res.json()
PATCH
/projects/:id/segments/:segment_id
with name, query or both: rename a segment, or save new conditions into it.
curl -X PATCH https://app.hunchwire.com/projects/:id/segments/:segment_id \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.patch(
"https://app.hunchwire.com/projects/:id/segments/:segment_id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/segments/:segment_id", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
DELETE
/projects/:id/segments/:segment_id
Delete a segment. The people in it stay.
curl -X DELETE https://app.hunchwire.com/projects/:id/segments/:segment_id \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.delete(
"https://app.hunchwire.com/projects/:id/segments/:segment_id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/segments/:segment_id", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
GET
/projects/:id/people.csv
Export the same list as a CSV file.
Details
- It has the same columns, except
id. It addsinvited_at,clicked_atandwave: their newest invite, and the wave it came in.ad_platformandadname the ad they first came through, or are blank.phoneandfirst_namefollowemail; a person by number has a blankemail. It adds one column per question asked now, headed with its wording. - Takes everything the list does:
?q=,days, filters,state,segmentandids[]. It's never paged. - Export works on every plan.
curl https://app.hunchwire.com/projects/:id/people.csv \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/people.csv",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people.csv", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
GET
/projects/:id/people/:person_id
Get one person's profile.
Show 8 details
- Any of their submission ids works. Returns 404 for anyone not in the project.
- Returns everything in their row, their newest
invite, andhistory, newest first. - Each
historyentry hasat,kind,textandby. awaiting_confirmation:truewhen they signed up again after being deleted and haven't confirmed. Until they do, only the confirmation is emailed.rejoining:truewhen they were removed, signed up again by email, and haven't clicked the confirmation yet. Clicking it puts them back on the list.kind:joined,invited,clicked(the invite),joined_product(the founder's product reported they joined),friend(a friend who counts joined through their link, or their link let someone waiting in),removed,restored,rejoined(they signed up again and confirmed, so they're back),moved,reward_given,referral_approvedorreferral_rejected.text: the entry as the founder's screen says it. When a friend's link let someone in, theirinvitedentry names whose link it was: "Let in through ana@example.com's link". Older ones read "Let in through a friend's link".by: who made the change. It's an agent's name, a founder's email, ornullfor what happened on its own. "you" never appears here.
curl https://app.hunchwire.com/projects/:id/people/:person_id \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/people/:person_id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people/:person_id", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
PATCH
/projects/:id/people
Remove many people at once, or send "waiting" to restore them. Only do this when the founder asks.
Details
- Instead of
ids, pick a whole view by its conditions:segment,q,filter,excludeandin.inis the state the view lists, sincestateis the one to set. Adddays(30 by default), orall=1for everyone in the period. - Returns
stateandchanged, how many people changed. - Returns 422 when you pick no one.
curl -X PATCH https://app.hunchwire.com/projects/:id/people \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state": "removed", "ids": [12, 15]}'
import os, requests
r = requests.patch(
"https://app.hunchwire.com/projects/:id/people",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"state": "removed", "ids": [12, 15]},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"state": "removed", "ids": [12, 15]}),
})
const data = await res.json()
PATCH
/projects/:id/people/:person_id
Move someone in line, here to place 5. Only do this when the founder asks.
Details
- Hunchwire stores how many places that is (
moved). Later friends and sign-ups can still shift them a place or two. - Their History shows each move. It never changes anyone's friends.
- Returns 422 for a place outside 1 to the number of people in line, or for someone removed.
curl -X PATCH https://app.hunchwire.com/projects/:id/people/:person_id \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"place": 5}'
import os, requests
r = requests.patch(
"https://app.hunchwire.com/projects/:id/people/:person_id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"place": 5},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people/:person_id", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"place": 5}),
})
const data = await res.json()
PATCH
/projects/:id/people/:person_id
or {"state": "waiting"}: remove someone from the list, or restore them. Only do this when the founder asks.
Details
- Removed people keep their data and leave the line (
placeisnull). Waves don't invite them. They come back if they sign up again and confirm (see Confirm sign-ups). - Returns the profile row. Returns 422 for any other state.
curl -X PATCH https://app.hunchwire.com/projects/:id/people/:person_id \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state": "removed"}'
import os, requests
r = requests.patch(
"https://app.hunchwire.com/projects/:id/people/:person_id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"state": "removed"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people/:person_id", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"state": "removed"}),
})
const data = await res.json()
Each read of emails shows on the founder's Agents page, once an hour per screen and project. That covers People, its CSV, Blocked, and Analytics' top_referrers.
POST
/projects/:id/purge_requests
in the body, or {"phone": "+14165550123"} for someone who signed up by number: delete that person from the project. Only do this when the founder or the owner asks.
Show 7 details
- It also deletes the same person's other spellings: +tags, Gmail dots and googlemail.com.
- It deletes their submissions, blocked sign-ups and invites, and the visits and clicks from their browser. Visits and clicks from a browser that someone else still uses to sign up stay.
- This can't be undone. A person who unsubscribes from an email is deleted the same way.
- A record of the request stays, as a keyed hash, never the email or number. The studio never emails that address again, unless it signs up to this project again and confirms through the link Hunchwire emails.
- Returns
201with{"status": "completed", "purged": {"submissions": 2, "visits": 5, "clicks": 1, "blocked_signups": 0, "answers": 1, "invites": 0, "people": 0, "reward_marks": 0, "referral_reviews": 0, "webhook_deliveries": 0}}. status:completed,not_foundorfailed.purged: how many rows were removed of each kind:submissions,visits,clicks,blocked_signups,answers,invites,people(a removal or move recorded for them),reward_marks(rewards marked given),referral_reviews(decisions on flagged referrals) andwebhook_deliveries(webhook events about them).
curl -X POST https://app.hunchwire.com/projects/:id/purge_requests \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "…"}'
import os, requests
r = requests.post(
"https://app.hunchwire.com/projects/:id/purge_requests",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"email": "…"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/purge_requests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"email": "…"}),
})
const data = await res.json()
| Status | Body | Meaning |
|---|---|---|
201 |
"status": "completed" |
The person was deleted. |
404 |
"status": "not_found" |
No one in the project has that email. Hunchwire still records the request. |
500 |
"status": "failed" |
The purge failed and nothing was removed. Try again. |
GET
/projects/:id/blocked?days=30
List the sign-ups Hunchwire refused, newest first. They never count as people.
Details
daysis 7, 14 or 30 (the default). Hunchwire keeps them 30 days.- Each has
emailorphone(the other isnull),form,reason,page_urlandcreated_at. reason:bot(filled the hidden field),disposable(a throwaway email domain) orno_mail(the domain can't receive mail).- For
disposableandno_mail, the visitor saw an error, so they could fix a typo. Bots saw the usual success.
curl https://app.hunchwire.com/projects/:id/blocked?days=30 \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/blocked?days=30",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/blocked?days=30", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
Hunchwire emails a project's people only from the founder's own verified domain, never from Hunchwire's. Hunchwire's own project sends from hunchwire.com.
Each email names who's asking (sender) and the founder's mailing address, and has a one-click unsubscribe, as Canada's anti-spam law requires. Unsubscribing deletes that person from the project. Hunchwire never again emails anyone who asked to be deleted, unsubscribed, bounced or complained.
A deleted or unsubscribed person can sign up to the same project again. They get the confirmation email and nothing else. Clicking its link confirms it's them, and then that project emails them as normal. Other projects still never email them.
Every send returns sent and skipped. They are counts, with skipped broken down by reason (plan, suppressed, unconfirmed, phone for numbers…). They never include addresses. unconfirmed counts people who signed up again after being deleted and haven't confirmed yet.
Set up email
- Add the domain and its email records (see Your domain).
- Call
PATCH /projects/:idwithmail_fromandmailing_address.mail_from: an address at that domain, likehello@tryloop.app. Email comes from it, and replies go to it.mailing_address: one line. A PO box or virtual mailbox is fine.
- Read
email.blockerinGET /projects/:id/domain. It names the first thing missing, in the order below. Tell the founder the next step.
email.blocker |
What's missing |
|---|---|
off |
Hunchwire hasn't turned on email yet. |
plan |
A paid plan. |
inactive |
The account needs a plan again. |
no_domain |
A domain. |
unverified |
Verified records: some are still waiting. |
paused |
Sending is paused. |
no_mail_from |
mail_from. |
no_sender |
Who's asking, or the mailing address. |
Send by hand
A founder can skip email setup and send invite links themselves. Hunchwire then stops asking them to set up email, and its screens lead with copying links and exporting addresses.
PATCH
/projects/:id
Send by hand.
Show 5 details
- Send
falseto have Hunchwire ask about email setup again. - Adding a domain turns it off.
- Invites and waves still make every link. Give the founder each
url, or the wave's CSV. - To write to people, give the founder the addresses from
GET /projects/:id/people/emails.csv. - Sign-ups aren't confirmed by email.
curl -X PATCH https://app.hunchwire.com/projects/:id \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"project": {"email_by_hand": true}}'
import os, requests
r = requests.patch(
"https://app.hunchwire.com/projects/:id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"project": {"email_by_hand": True}},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"project": {"email_by_hand": true}}),
})
const data = await res.json()
Confirm sign-ups
Once a project can email its people (email.ready), each new sign-up gets a confirmation link from the founder's domain, once per person. The wording is fixed: "Confirm you want to hear about …". The form's confirmation then tells them to check their inbox instead of naming the address.
Clicking the link on the project's host (/confirm/<token>, good for 30 days) confirms them. On the free month, sign-ups are collected, not confirmed.
A number never gets a confirmation. Your product confirms it by sending confirmed (see Sign people up and look them up).
Someone already on the list who signs up again through a friend's link gets one more confirmation, even if they confirmed before. Clicking it can let them in (see Let friends straight in). It goes at most once every 10 minutes per person.
A removed person who signs up again comes back once they prove the contact. An email gets one more confirmation link, even if they confirmed before. Clicking it puts them back on the list, as Restore does. A number comes back on a sign-up with confirmed. Until then, and while the project can't email its people, they stay removed.
POST
/projects/:id/confirmations
Send the confirmation to everyone who missed it.
Details
- It goes once to each person who signed up in the last 30 days and never got one: before the domain was verified, or on the free month.
- Removed people are left out.
- Only do this when the founder asks.
- Returns
sentandskipped.
curl -X POST https://app.hunchwire.com/projects/:id/confirmations \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.post(
"https://app.hunchwire.com/projects/:id/confirmations",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/confirmations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
Email people
People › Emails writes to everyone in a segment, a People view, or people picked by id. An email reaches Waiting and Invited people only, never removed ones. Hunchwire works out who when it sends.
Emails are plain text, with links made clickable. Hunchwire adds the footer and unsubscribe link. Write the email with the founder, and send it only when they say so.
GET
/projects/:id/people/emails
List sent and held emails, newest first.
Show 10 details
id,subjectandbody.to: who it was for:segment,querywithdays, orids.to_label: who it was for, as the screen says it.state:sent, orhelduntil it can send. A held email waits for the plan to allow email, then for the domain to be verified and email to be set up.sent: how many people it went to.skipped: counts by reason.delivered, andbounced(bounced or complained).sent_at,created_atandby.- It never says who got the email. It has no clicks or opens, because Hunchwire doesn't track them.
- For a founder who sends by hand, add
.csvand pick people as inPOSTbelow. It returnsemailandstatefor each person an email would reach, never a number. It needs emails.
curl https://app.hunchwire.com/projects/:id/people/emails \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/people/emails",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people/emails", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
POST
/projects/:id/people/emails
Send an email.
Show 6 details
segmentis a saved segment, ortop_referrers. Instead ofsegment, pick a view within(waitingorinvited),q,filter,excludeanddays, or pick people withids(person ids).daysis all time unless you set it.- The subject is 1 to 150 characters. The body is 1 to 10,000.
- Returns the email, as above, with 201.
- On a plan without email, Hunchwire answers
402plan_required(see Plans and gates). The email is held. It sends by itself once the founder upgrades and email is set up (see Set up email), within a few minutes of the domain being verified. - Any other blocker (see
email.blocker) answers 422 with its reason. So does picking no one. - Add
"test": trueto send one copy to the founder instead. A test is free on every plan, but it still needs the domain. It returns{"test": "sent", "to": "…"}and isn't saved. Send a test first, and ask the founder to check it.
curl -X POST https://app.hunchwire.com/projects/:id/people/emails \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject": "Can we talk for 15 minutes?", "body": "Hi,\n\n…", "segment": <id>}'
import os, requests
r = requests.post(
"https://app.hunchwire.com/projects/:id/people/emails",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
json={"subject": "Can we talk for 15 minutes?", "body": "Hi,\n\n…", "segment": <id>},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/people/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"subject": "Can we talk for 15 minutes?", "body": "Hi,\n\n…", "segment": <id>}),
})
const data = await res.json()
/unsubscribe/<token> and /confirm/<token> on the project's own host are for the people who get the email, not for agents.
Made from skill.md, the file agents read. Add .md to any page's address for its Markdown.