API reference
API
Sign people up and look them up from your own server.
Use your agent. Paste this into it:
Read hunchwire.com/docs/api and connect my app to my project with the API.
On this page
APIAPI
Sign people up and look them up from your own server, with the project's secret key. The API works with every way. With own_site_api, it comes first in Build › Where it lives.
GET
/projects/:id/setup/site/api
Get the keys and code examples.
Show 5 details
public_key: the project'sembed_key. It goes in the browser, in the embed.secret_key_set: whether a secret key exists.secret_key_hint: its last 4 characters.secret_key_created_at: when it was made.api_url: the API's base address.examples: the two calls, each withlanguage(curl,JavaScriptorPython) andcode.live_check: the newest sign-up or refused site, as inGET /projects/:id/setup.
curl https://app.hunchwire.com/projects/:id/setup/site/api \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.get(
"https://app.hunchwire.com/projects/:id/setup/site/api",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/setup/site/api", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
POST
/projects/:id/setup/site/api/key
Make the secret key, or replace it.
Details
- Returns
secret_keyonce. Hunchwire keeps only a digest, so it can't show the key again. - A new key replaces the old one at once. Calls with the old key fail until the server has the new one.
- Put it in the founder's server environment as
HUNCHWIRE_SECRET_KEY. Never put it in a page or a repository. - Only make or replace it when the founder asks.
curl -X POST https://app.hunchwire.com/projects/:id/setup/site/api/key \
-H "Authorization: Bearer $HUNCHWIRE_TOKEN"
import os, requests
r = requests.post(
"https://app.hunchwire.com/projects/:id/setup/site/api/key",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_TOKEN']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/projects/:id/setup/site/api/key", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_TOKEN}`,
},
})
const data = await res.json()
Authenticate
Send the secret key on every call, from a server:
The base address is https://app.hunchwire.com/v1. Hunchwire refuses a call that carries a browser Origin header. In a browser, use the embed.
Authorization: Bearer sk_…
Content-Type: application/json
The person
A person has one contact: an email, or a phone in international format (+14165550123). Send exactly one, and first_name if you have it. Hunchwire never emails or texts a number: your product does the texting. Every person you get back, from a call or a webhook, has email, phone and first_name. The contact you didn't use is null.
id: the person's id in Hunchwire. Store it.state:waiting,invitedorremoved, as on People.place: their place in line while the line moves, elsenull.code: their referral code.link: their link to share. It uses the project's referrallinkwhen one is set.friends: friends who joined through their link and count, plus people their link let in while waiting. A friend counts once both have confirmed (see Referrals).pending_friends: friends who confirmed and count the moment this person confirms.referred_by: the id of whoever brought them, ornull. You never get anyone else's contact.access: whether they're in.grantedis true or false.viaisinvite(an invite exists),referral(a friend's link let them in) orfriends(they brought enough friends).atis when.rewards: what they earned through referrals, each withkey,label,reached_atandgiven_at.joined_product_at: when your product reported they joined it, ornull.signed_up_at: their first sign-up.confirmed: whether they confirmed their email, or texted you from their number.
{ "id": 4812, "email": "maya@fernwood.studio", "phone": null, "first_name": "Maya", "list": null,
"state": "waiting", "place": 148, "code": "k7q2m9xa", "link": "https://tryloop.app/?ref=k7q2m9xa",
"friends": 2, "pending_friends": 0, "referred_by": 4790, "access": { "granted": false, "via": null, "at": null },
"rewards": [], "joined_product_at": null, "signed_up_at": "2026-10-11T14:02:11Z", "confirmed": false }
POST
/v1/people
Sign someone up.
Show 11 details
- Takes
emailorphone. Optional:first_name(up to 50 characters),ref,list,consent_text(up to 500 characters),confirmed,form(defaultwaitlist), and the visit:visitor_id,page_url,referrer,utm_source,utm_medium,utm_campaign,utm_content. refis a friend's code.listworks like the embed'sdata-list.consent_textis the consent wording your form showed.- For a number, show and send consent wording like: "By continuing, you agree to get messages from Loop at this number. Message and data rates may apply. Reply STOP to opt out."
- Send
"confirmed": trueonce the person has texted you from the number. Their first text proves it's theirs. Only a number takes it. An email confirms by the link Hunchwire sends. - On a number,
confirmedtrue means the person texted you from that number. - Returns the person: 201 when new, 200 when the contact is already on the list.
ref_statussays what happened toref:none(none sent),counted(recorded as their referrer; it counts once both have confirmed),held(recorded, but held for review),already_on_list(only a first sign-up can be referred),own_codeorunknown.invite: when a friend's link let them in, their invite'sid,urlandexpires_at, elsenull. Welcome them right away, then report the join.- A number already on the list, sent again with
refandconfirmed, can be let in through that link. An email already on the list is let in only when they click the link Hunchwire emails them. - Without
consent_text, Hunchwire records the project's own consent line for an email sign-up. - It runs the form's spam checks. A throwaway or unreachable email domain is refused and listed under People › Blocked.
curl -X POST https://app.hunchwire.com/v1/people \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "maya@fernwood.studio", "first_name": "Maya", "ref": "k7q2m9xa", "visitor_id": "6f1c…"}'
import os, requests
r = requests.post(
"https://app.hunchwire.com/v1/people",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
json={"email": "maya@fernwood.studio", "first_name": "Maya", "ref": "k7q2m9xa", "visitor_id": "6f1c…"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/people", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"email": "maya@fernwood.studio", "first_name": "Maya", "ref": "k7q2m9xa", "visitor_id": "6f1c…"}),
})
const data = await res.json()
GET
/v1/people/:id
Get a person by id.
curl https://app.hunchwire.com/v1/people/:id \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY"
import os, requests
r = requests.get(
"https://app.hunchwire.com/v1/people/:id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/people/:id", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
},
})
const data = await res.json()
GET
/v1/people?email=maya%40fernwood.studio
Get a person by contact.
Details
- For a phone number, send
?phone=%2B14165550123. A leading space is read as+. - Never put a contact in the path.
curl https://app.hunchwire.com/v1/people?email=maya%40fernwood.studio \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY"
import os, requests
r = requests.get(
"https://app.hunchwire.com/v1/people?email=maya%40fernwood.studio",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/people?email=maya%40fernwood.studio", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
},
})
const data = await res.json()
PATCH
/v1/people/:id
Set a person's first name. It's the only field you can change.
curl -X PATCH https://app.hunchwire.com/v1/people/:id \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name": "Maya"}'
import os, requests
r = requests.patch(
"https://app.hunchwire.com/v1/people/:id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
json={"first_name": "Maya"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/people/:id", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"first_name": "Maya"}),
})
const data = await res.json()
POST
/v1/people/:id/joined_product
Tell Hunchwire your product let this person in.
Details
- Use it when people join without an invite link, such as in a chat.
- It uses up their open invite, if they have one.
- Returns the person with
joined_product_atset. A second call changes nothing.
curl -X POST https://app.hunchwire.com/v1/people/:id/joined_product \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY"
import os, requests
r = requests.post(
"https://app.hunchwire.com/v1/people/:id/joined_product",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/people/:id/joined_product", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
},
})
const data = await res.json()
POST
/v1/invites/redeem
or {"token": "…"}: tell Hunchwire an invited person joined your product.
Details
- Send
code, the end of their invite link, ortoken, thehw_tokenyour app already checked (see Let people in). - Returns
person(nullwhen the invite went to someone not on the list) andinvite, withid,email,phone(one isnull),state(joined) andredeemed_at. - Errors: 409
usedwhen it was already redeemed. 410expiredwhen the invite (14 days) or the token (10 minutes) expired. 404not_foundfor a code or token that isn't this project's.
curl -X POST https://app.hunchwire.com/v1/invites/redeem \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "…"}'
import os, requests
r = requests.post(
"https://app.hunchwire.com/v1/invites/redeem",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
json={"code": "…"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/invites/redeem", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({"code": "…"}),
})
const data = await res.json()
DELETE
/v1/people/:id
Delete a person's data in this project.
Details
- Use it when someone asks your product to delete them, or texts STOP.
- It's the same as Delete on their profile. Hunchwire never re-adds them. It emails them again only if they sign up again and confirm.
- Returns 204.
curl -X DELETE https://app.hunchwire.com/v1/people/:id \
-H "Authorization: Bearer $HUNCHWIRE_SECRET_KEY"
import os, requests
r = requests.delete(
"https://app.hunchwire.com/v1/people/:id",
headers={"Authorization": f"Bearer {os.environ['HUNCHWIRE_SECRET_KEY']}"},
)
r.json()
const res = await fetch("https://app.hunchwire.com/v1/people/:id", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.HUNCHWIRE_SECRET_KEY}`,
},
})
const data = await res.json()
Limits
| Calls | Limit |
|---|---|
| Sign-ups | 30 a minute per project |
| Lookups and updates | 300 a minute per project |
Errors
An error returns {"error": "<code>", "message": "…"}.
| Status | error |
When |
|---|---|---|
| 401 | unauthorized |
No secret key, or not this project's |
| 403 | browser_origin |
The call came from a browser |
| 403 | not_live |
The project isn't live yet |
| 404 | not_found |
No one with that id or contact, or no such invite |
| 409 | used |
The invite was already redeemed |
| 410 | expired |
The invite or token expired |
| 422 | contact_required, two_contacts |
Send exactly one of email or phone |
| 422 | invalid_email |
The email isn't valid |
| 422 | invalid_phone |
The number has no country code, or isn't a mobile number's length |
| 422 | confirm_by_link |
confirmed was sent for an email |
| 422 | invalid_first_name, invalid_list, unknown_form |
A field is out of range |
| 422 | disposable, no_mail |
The email domain is refused |
| 429 | rate_limited |
Too many calls. Wait a minute |
Made from skill.md, the file agents read. Add .md to any page's address for its Markdown.