Contact forms
The one-way SMS, email, and Discord contact endpoints a public profile exposes, with the mandatory Cloudflare Turnstile token and the rate limits that protect them.
Contact forms
A published profile can expose up to three one-way contact forms. A visitor fills one in on the public page and the message is delivered to you, the owner, over SMS, email, or Discord. The visitor never sees your phone number, email address, or Discord channel. These endpoints are meant to be called from the visitor's browser on the public profile, but they are documented here so you can wire them into a custom-built contact form on your own profile.
- SMS sends with the owner's SMS key and debits their SMS credits.
- Email sends with the owner's email key and debits their email credits.
- Discord posts an embed to the owner's configured channel; no credit cost.
Common requirements
Cloudflare Turnstile is mandatory. Every contact POST must include a valid turnstileToken. This replaces an API key: it proves the request came from a real browser, not a script. Render a Turnstile widget on your form and send the token it produces. A missing or failing token is rejected with 400 and the generic error Anti-spam check failed (the same shape as other validation errors, so bots get no hint that Turnstile is running).
Owner setup. Each channel only works once you have set it up in the dashboard:
- SMS: an enabled SMS key plus a 2FA-verified phone, and a positive
sms_creditsbalance. - Email: a verified email address plus an enabled email key, and email credits.
- Discord: a configured notification channel with the Hep.gg bot present in that channel's server.
If a channel is not set up, the endpoint returns 400 with a message telling the visitor the form is not active. It never reveals which piece is missing.
Rate limits. All three forms enforce the same triple-bucket limit, evaluated before Turnstile so flagrant abuse is cheap to reject:
| Bucket | Limit |
|---|---|
| Per IP + profile, burst | 3 per minute |
| Per IP + profile, daily | 30 per day |
| Per owner, daily | 50/day (SMS), 100/day (email and Discord) |
A throttled request returns 429. The per-IP burst bucket also sends a Retry-After header (in seconds).
Response envelope. Success is the standard { "ok": true, "data": { ... } }. Errors are { "ok": false, "error": "<message>" }.
SMS
https://hep.gg/api/v1/profiles/:profileID/contact/smsPublicSends a text to the owner's verified phone. Each send debits one of the owner's SMS credits, so the per-owner daily cap (50) is the real cost ceiling.
profileIDprofiles.id).messageturnstileTokencurl -X POST https://hep.gg/api/v1/profiles/PROFILE_ID/contact/sms \
-H "Content-Type: application/json" \
-d '{"message":"Hey, loved your site!","turnstileToken":"TURNSTILE_TOKEN"}'const res = await fetch("https://hep.gg/api/v1/profiles/PROFILE_ID/contact/sms", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Hey, loved your site!", turnstileToken: TURNSTILE_TOKEN }),
});
const body = await res.json(); // { ok: true, data: { queued: true } }Responses
| Status | Meaning |
|---|---|
200 | { "ok": true, "data": { "queued": true } } |
400 | Message required, Turnstile failed, or owner has not set up SMS |
402 | Owner is out of SMS credits |
404 | Profile not found, not public, or suspended |
429 | Rate-limited (burst sends Retry-After) |
502 | SMS delivery failed or was unavailable |
https://hep.gg/api/v1/profiles/:profileID/contact/emailPublicEmails the owner's verified address through the hep.gg SMTP pipeline. The visitor's email is set as the reply-to so the owner can reply directly; the owner's address is never exposed to the visitor.
profileIDprofiles.id).emailsubjectmessageturnstileTokencurl -X POST https://hep.gg/api/v1/profiles/PROFILE_ID/contact/email \
-H "Content-Type: application/json" \
-d '{"email":"visitor@example.com","subject":"Hello","message":"Great profile!","turnstileToken":"TURNSTILE_TOKEN"}'const res = await fetch("https://hep.gg/api/v1/profiles/PROFILE_ID/contact/email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "visitor@example.com",
subject: "Hello",
message: "Great profile!",
turnstileToken: TURNSTILE_TOKEN,
}),
});
const body = await res.json(); // { ok: true, data: { queued: true, messageId: "..." } }Responses
| Status | Meaning |
|---|---|
200 | { "ok": true, "data": { "queued": <boolean>, "messageId": "<id>" } } |
400 | Invalid email, message required, Turnstile failed, or owner has not set up email |
402 | Owner is out of email credits |
404 | Profile not found, not public, or suspended |
429 | Rate-limited (burst sends Retry-After) |
Other rejections from the email pipeline pass through their own status code and error message.
Discord
https://hep.gg/api/v1/profiles/:profileID/contact/discordPublicPosts a one-way embed into the owner's configured notification channel through the Hep.gg bot. No credit cost; the rate-limit budget is the only abuse cap. The owner must have a channel configured and the Hep.gg bot present in that channel's server.
profileIDprofiles.id).handleAnonymous visitor.messageturnstileTokencurl -X POST https://hep.gg/api/v1/profiles/PROFILE_ID/contact/discord \
-H "Content-Type: application/json" \
-d '{"handle":"Jamie","message":"Pinging you on Discord!","turnstileToken":"TURNSTILE_TOKEN"}'const res = await fetch("https://hep.gg/api/v1/profiles/PROFILE_ID/contact/discord", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: "Jamie", message: "Pinging you on Discord!", turnstileToken: TURNSTILE_TOKEN }),
});
const body = await res.json(); // { ok: true, data: { queued: true } }Responses
| Status | Meaning |
|---|---|
200 | { "ok": true, "data": { "queued": true } } |
400 | Message required, Turnstile failed, or owner has not set up Discord |
404 | Profile not found, not public, or suspended |
429 | Rate-limited (burst sends Retry-After) |
502 | Delivery failed |