# Let people in

Invite people from your waitlist into your own product. Only people you invite get in.

## Let people in

Invite people into the founder's own product, so only invited people get in. It works the same whether the waitlist is on hosted pages or the founder's own site.

Hunchwire checks the invite, then sends the person to the project's address with `?hw_token=…`. The token is a 10-minute HS256 JWT signed with the project's secret. It carries `email` (or `phone`, for an invite to a number), `project` (the project's `embed_key`), `invite_id`, `iat` and `exp`. The founder's app verifies it locally with any JWT library, and signs up only that email or number.

Invites are not referrals: they add no one to the line and move no one.

### How an invite link works

1. The founder sends the link. The invite is `invited`.
2. The person opens it. Hunchwire sends them on with a fresh token, and the invite is `clicked`.
3. They can open it again as often as they need, say if your app was down or they stopped halfway. Each open makes a new 10-minute token.
4. Your product reports that they joined, with `POST /v1/people/:id/joined_product` or `POST /v1/invites/redeem` (see API). The invite is `joined`, and the link stops working.
5. A link nobody joins with expires after 14 days.

Every token carries the invite's email, or `phone` for an invite to a number. Your app signs up only that contact, so a forwarded link can't let anyone else in. Report each join, or the link keeps working until it expires.

The email in a token is the address the person typed. Confirm it in your sign-up, as you would for anyone. A `phone` is the number they signed up with: it's proven once they text you from it. Invites also come from friends' links, when friends get access (see Referrals).

### Set up the address

- `GET /projects/:id/setup/site/let-in`: get the setup. *analytics*
  - `address`: where invites send people, or `null` while it's off.
  - `secret_set`: whether the project has a secret.
  - The secret itself is for the founder only. Ask them to copy it from **Setup › Connect › Let people in** into the app's environment, such as `HUNCHWIRE_SECRET`.
- `PATCH /projects/:id/setup/site/let-in` with `{"let_in": {"address": "https://yourapp.com/join"}}`: turn it on, or change the address. *manage*
  - The address must be https. `http://localhost` works for testing.
  - The first address makes the secret.
  - An empty address turns it off, and every invite link stops working.
- `POST /projects/:id/setup/site/let-in/rotate`: make a new secret, at once. Only do this when the founder asks. *manage*
  - The old secret stops working, so invites fail until the app has the new one.
- `POST /projects/:id/setup/site/let-in/test`: test an invite. *manage*
  - Hunchwire calls the address twice: with a test token (`invite_id` `"test"`, email `test@example.com`), and with one signed with a wrong secret.
  - You can test 10 times a minute.
  - `status`: the HTTP status the app replied with, or `null`.
  - `message`: the result, in a sentence for the founder.
  - `test_url`: with `unreachable`, a link with a real test token to open in a browser. Otherwise `null`.
  - `result` is one of these:

| `result` | Meaning |
|---|---|
| `accepted` | The app took the real token and turned away the wrong one. |
| `rejected` | The app turned away the real token. Check the secret. |
| `not_checking` | The app took both. It isn't verifying tokens. |
| `unreachable` | Hunchwire can't call it, such as at `localhost`. |

### Manage invites

An invite has these fields:

- `email` or `phone`: who it's for. The other is `null`.
- `state`: `invited`, `clicked` (they opened it and were sent on), `joined` (the founder's product reported they joined) or `expired`.
- `url`: the link, on the project's own host.
- `sent_at`, `clicked_at`, and `expires_at` (14 days).
- `redeemed_at`: when the product reported the join, or `null`.
- `wave_id`: the wave it was made in, or `null`.
- `via`: `referral` when a friend's link let them in, else `null`.

An invite works until the person joins or it expires. Opening it again gives a new token for the invite's email or phone.

- `GET /projects/:id/invites`: list the project's invites, newest first, at most 200. *emails*
- `POST /projects/:id/invites` with `{"invite": {"email": "maya@fernwood.studio"}}`: invite anyone, on the waitlist or not. *manage* and *emails*
  - For a mobile number, send `{"invite": {"phone": "+14165550123"}}`. A number is never emailed (`emailed` is `phone`): text it the `url`.
  - To invite someone on the list, send `{"invite": {"person": <their id>}}` instead. It uses the email or number they signed up with, exactly.
  - Hunchwire never swaps one address for another. A `person` sent with a different `email` returns 422, "That email doesn't match this person."
  - Inviting the same email again returns the same invite (200) until it's used or expires. Otherwise it returns 201 with a new one.
  - Returns 422 until Let people in has an address.
  - `emailed`: `true` when the link went out by email. Someone on the list gets it once, from the founder's domain (see Email). Inviting again emails only if it never went out.
  - When `emailed` isn't `true`, it says why: `not_on_list` (only people who signed up can be emailed), `phone` (an invite to a number: text the `url`), `removed`, `suppressed`, `unconfirmed`, or an `email.blocker` reason such as `plan`. Then give the `url` to the founder to share by hand.
  - `unconfirmed`: they signed up again after being deleted. Hunchwire emails the invite once they confirm their email. Don't share it by hand.

### Invite in waves

A wave lets many people in at once.

- `GET /projects/:id/people/waves`: list invite waves, newest first. *emails*
  - Each has `id`, `name`, `created_at`, `invited` and `by` (who made it).
  - `picked`: how people were picked: `top`, `segment` or `ids`.
  - `message`: what its invite email says, or `null` for the default.
  - `emailed_at`: when its invite emails went out, or `null`.
  - `email_held`: true while its emails are held. They go out by themselves once the plan allows email and email is set up.
  - `by_text`: of those invited, people who signed up by number. Hunchwire never emails them, so text them their links.
  - `clicked`: how many opened the link. It never counts email opens.
  - Each invite's `state` says whether the product reported them joined.
- `POST /projects/:id/people/waves` with `{"name": "Founding 50", "top": 50}`: invite many people at once. Only do this when the founder asks. *manage* and *emails*
  - `top` picks the first N waiting people by place, from 1 to 1000. Instead, send `"segment": <id>` for everyone waiting in a saved segment, whenever they joined, or `"ids": [...]`.
  - Only waiting people get a link. Hunchwire skips removed people, people already invited, and anyone who asked to be deleted. `skipped` counts them as `removed`, `invited` and `suppressed`. Someone who signed up again after being deleted gets a link, emailed once they confirm.
  - Each link works for the email or number they signed up with, for 14 days, until they join the founder's product.
  - Returns the wave with its `invites` (as above), 201.
  - Returns 422 until Let people in has an address, or when no one is left to invite.
  - Hunchwire emails each person their link from the founder's domain. The email says "You're in. Welcome to …", then the wave's `message`, then a Get started button.
  - People who signed up by number get no email: `by_text` counts them, and your product texts their links.
  - `message` is up to 2,000 characters. Without one, the email uses a plain default.
  - `emailed`: how many emails went out. `email_skipped`: the rest, counted by reason (`suppressed`, `unconfirmed`, or an `email.blocker` reason). Each `unconfirmed` invite is emailed once that person confirms their email.
  - Hunchwire makes the links either way. Give the founder the ones that weren't emailed, or the wave's CSV.
  - On a plan without email, `email_skipped` is `{"plan": N}`. The emails are held. They go out by themselves once the founder upgrades and email is set up, within a few minutes of the domain being verified.
  - A wave made while email isn't set up, on a plan with email, is never emailed later. Share its links by hand.
  - Add `"test": true` to send one sample invite email to the founder who connected you instead. It's free on every plan, and nothing is made. It returns `{"test": "sent", "to": "…"}`.
- `GET /projects/:id/people/waves/:wave_id`: get one wave with its `invites`. *emails*
  - `.csv` returns `email`, `link`, `state`, `clicked_at` and `phone` for each invite. A number's `email` is blank.

### Check the token

In the founder's app, check the token like this. The **Let people in** screen also shows Python and Ruby.

```js
import jwt from &quot;jsonwebtoken&quot;;
// On your address (?hw_token=…): check it, and remember who it&#39;s for.
const { email, phone, invite_id } = jwt.verify(req.query.hw_token, process.env.HUNCHWIRE_SECRET, { algorithms: [&quot;HS256&quot;] });
req.session.invited = email || phone; // phone, like +14165550123, for an invite to a number
// On sign-up: only that email gets an account (for a number, compare the number they verified).
if (form.email.trim().toLowerCase() !== req.session.invited) return refuse(`This invite is for ${req.session.invited}. Use that email, or ask for an invite for this one.`);
```
