# People

Everyone who signed up: where they came from, what they answered, and the emails you send them.

## 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.

### List people

- `GET /projects/:id/people`: list everyone who submitted a form, one row per person, newest first. *emails*
  - Takes filters and `days`, like Analytics. Without `days`, it lists the last 30 days. `days=all` lists everyone.
  - A person is listed when they submitted in the period. With filters, only submissions from the browsers the filters pick count.
  - `page=2` returns the second 50. The founder's screen shows 50 a page. Without `page`, you get everyone.
  - `?q=ada` keeps only emails containing `ada`. A number matches however it's typed: `?q=416 555` finds `+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=invited` or `state=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 as `q_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_up` lists 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 is `null`.
    - `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`, `invited` or `removed`. `invited` means an invite exists for the exact email they signed up with. `removed` means 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, or `null`.
    - `language`: their browser's language when they signed up, such as `en` or `fr-CA`.
    - `city`: where they signed up, from Cloudflare. `language` and `city` are `null` when not known. Hunchwire never keeps the IP.
    - `channel`, `source` and `campaign`: where they first came from. `campaign` is the campaign link, or `null` when untagged.
    - `visitor_ids`: the browsers they signed up from. Open one with `GET /projects/:id/analytics/visitors/:visitor_id` to 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's `null` once 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, or `null`.
    - `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 has `key`, `question` (its wording when they answered), `answer` and `answered_at`. It's empty when they answered nothing.
    - `invite`: their newest invite into the founder's product (see Let people in), or `null`.
    - `confirmed_at`: when they confirmed their email, or `null`. It's the earliest across their submissions.
    - `consent_text`, `consent_version` and `consent_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 are `null` when none did.
    - `first_name`: the first name a sign-up sent, or `null`.
    - `list`: the list they joined, or `null`.
    - `via`: how they first signed up: `embed` (the form) or `api`.
    - `joined_product_at`: when the founder's product reported they joined it (see API), or `null`.
  - A person is an email with any `+tag` dropped, and for Gmail, its dots too. So `ada+1@gmail.com` shares a place with `ada@gmail.com`.

### 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. *analytics*
  - Each has `id`, `name`, `query`, `created_at` and `updated_at`.
  - `query`: its conditions, from `state`, `segment`, `q`, `filter` and `exclude`.
- `POST /projects/:id/segments` with `{"name": "Said Notion", "query": {"filter": {"answer": ["q_0qjfp5jp:Notion"]}}}`: save a segment. *manage*
  - 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.
- `PATCH /projects/:id/segments/:segment_id` with `name`, `query` or both: rename a segment, or save new conditions into it. *manage*
- `DELETE /projects/:id/segments/:segment_id`: delete a segment. The people in it stay. *manage*

### Export and open a profile

- `GET /projects/:id/people.csv`: export the same list as a CSV file. *emails*
  - It has the same columns, except `id`. It adds `invited_at`, `clicked_at` and `wave`: their newest invite, and the wave it came in. `ad_platform` and `ad` name the ad they first came through, or are blank. `phone` and `first_name` follow `email`; a person by number has a blank `email`. It adds one column per question asked now, headed with its wording.
  - Takes everything the list does: `?q=`, `days`, filters, `state`, `segment` and `ids[]`. It's never paged.
  - Export works on every plan.
- `GET /projects/:id/people/:person_id`: get one person's profile. *emails*
  - Any of their submission ids works. Returns 404 for anyone not in the project.
  - Returns everything in their row, their newest `invite`, and `history`, newest first.
  - Each `history` entry has `at`, `kind`, `text` and `by`.
  - `awaiting_confirmation`: `true` when they signed up again after being deleted and haven't confirmed. Until they do, only the confirmation is emailed.
  - `rejoining`: `true` when 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_approved` or `referral_rejected`.
  - `text`: the entry as the founder's screen says it. When a friend's link let someone in, their `invited` entry 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, or `null` for what happened on its own. "you" never appears here.

### Change people

- `PATCH /projects/:id/people` with `{"state": "removed", "ids": [12, 15]}`: remove many people at once, or send `"waiting"` to restore them. Only do this when the founder asks. *manage* and *emails*
  - Instead of `ids`, pick a whole view by its conditions: `segment`, `q`, `filter`, `exclude` and `in`. `in` is the state the view lists, since `state` is the one to set. Add `days` (30 by default), or `all=1` for everyone in the period.
  - Returns `state` and `changed`, how many people changed.
  - Returns 422 when you pick no one.
- `PATCH /projects/:id/people/:person_id` with `{"place": 5}`: move someone in line, here to place 5. Only do this when the founder asks. *manage* and *emails*
  - 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.
- `PATCH /projects/:id/people/:person_id` with `{"state": "removed"}` or `{"state": "waiting"}`: remove someone from the list, or restore them. Only do this when the founder asks. *manage* and *emails*
  - Removed people keep their data and leave the line (`place` is `null`). 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.

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`.

### Delete a person

- `POST /projects/:id/purge_requests` with `{"email": "…"}` 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. *manage* and *emails*
  - 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 `201` with `{"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_found` or `failed`.
  - `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) and `webhook_deliveries` (webhook events about them).

| 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. |

### Blocked sign-ups

- `GET /projects/:id/blocked?days=30`: list the sign-ups Hunchwire refused, newest first. They never count as people. *emails*
  - `days` is 7, 14 or 30 (the default). Hunchwire keeps them 30 days.
  - Each has `email` or `phone` (the other is `null`), `form`, `reason`, `page_url` and `created_at`.
  - `reason`: `bot` (filled the hidden field), `disposable` (a throwaway email domain) or `no_mail` (the domain can't receive mail).
  - For `disposable` and `no_mail`, the visitor saw an error, so they could fix a typo. Bots saw the usual success.

## Email

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

1. Add the domain and its email records (see Your domain).
2. Call `PATCH /projects/:id` with `mail_from` and `mailing_address`.
   - `mail_from`: an address at that domain, like `hello@tryloop.app`. Email comes from it, and replies go to it.
   - `mailing_address`: one line. A PO box or virtual mailbox is fine.
3. Read `email.blocker` in `GET /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` with `{"project": {"email_by_hand": true}}`: send by hand. *manage*
  - Send `false` to 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.

### 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. *manage* and *emails*
  - 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 `sent` and `skipped`.

### 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. *analytics*
  - `id`, `subject` and `body`.
  - `to`: who it was for: `segment`, `query` with `days`, or `ids`.
  - `to_label`: who it was for, as the screen says it.
  - `state`: `sent`, or `held` until 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`, and `bounced` (bounced or complained).
  - `sent_at`, `created_at` and `by`.
  - 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 `.csv` and pick people as in `POST` below. It returns `email` and `state` for each person an email would reach, never a number. It needs *emails*.
- `POST /projects/:id/people/emails` with `{"subject": "Can we talk for 15 minutes?", "body": "Hi,\n\n…", "segment": <id>}`: send an email. *manage* and *emails*
  - `segment` is a saved segment, or `top_referrers`. Instead of `segment`, pick a view with `in` (`waiting` or `invited`), `q`, `filter`, `exclude` and `days`, or pick people with `ids` (person ids). `days` is 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 `402` `plan_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": true` to 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.

`/unsubscribe/<token>` and `/confirm/<token>` on the project's own host are for the people who get the email, not for agents.
