HunchwireDocs hunchwire.com Sign in

Guides

Pages

Hunchwire hosts the site and your agent writes its pages. You can add your own domain.

View as Markdown

Use your agent. Paste this into it:

Read hunchwire.com/docs/pages and write a landing page for my idea.
On this page Pages Versions Your domain

Pages

A page has id, path, version, media_type, updated_at and content.

  • version: the key of the page's version. Only an HTML page of a project testing versions has one.
  • content: only when you read one page with GET. Adding or changing a page returns it without content, since you just sent it. Images have byte_size instead of content.

The path sets how a page is served:

  • A path ending in .css, .js, .svg, .txt or .md is served as that type. .txt is text/plain and .md is text/markdown.

  • An image path is served as its image type.

  • Any other path is an HTML page.

  • Hunchwire itself uses /embed.js, /e/…, /up, /robots.txt and /sitemap.xml.

  • /404 is the page a missing path shows, with status 404. Without one, visitors get a plain "Page not found" that links to /.

POST /projects/:id/pages Needs manage

Add a page.

Details
  • In a project testing versions, each HTML page belongs to one version. Send "version": "B" in page.
  • Without a version, the page is a 422. That way you never give one version a page the others lack by accident.
  • Files (.css, .js, images…) are shared by every version and take no version.
  • Versions never share HTML. To change a page in every version, change each one.
POST /projects/:id/pages
curl -X POST https://app.hunchwire.com/projects/:id/pages \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"page": {"path": "/pricing", "content": "<!doctype html>…"}}'

POST /projects/:id/pages Needs manage

with an image: upload it as a multipart file, not JSON.

Details
  • PNG, JPG, WebP, GIF or AVIF, up to 5 MB. The file's contents must match its extension.
  • Leave out page[path] to use /images/<file name>.
  • Show it on a page with <img src="/images/hero.png">.
  • To replace an image, PATCH the page with a new page[file].
POST /projects/:id/pages
curl -X POST https://app.hunchwire.com/projects/:id/pages \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"
Pages
curl -X POST https://app.hunchwire.com/projects/:id/pages \
  -H "Authorization: Bearer hw_…" \
  -F "page[path]=/images/hero.png" -F "page[file]=@hero.png"

GET /projects/:id/pages/:page_id Needs analytics

Get one page, with its content. Images return byte_size instead of content.

GET /projects/:id/pages/:page_id
curl https://app.hunchwire.com/projects/:id/pages/:page_id \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

PATCH /projects/:id/pages/:page_id Needs manage

Change a page's path or content.

PATCH /projects/:id/pages/:page_id
curl -X PATCH https://app.hunchwire.com/projects/:id/pages/:page_id \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

DELETE /projects/:id/pages/:page_id Needs manage

Remove a page.

Details
  • Returns 204. If live campaigns' ads lead to it, Hunchwire pauses them on Meta first and returns 200 with paused_campaigns. A path another version keeps doesn't count.
DELETE /projects/:id/pages/:page_id
curl -X DELETE https://app.hunchwire.com/projects/:id/pages/:page_id \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

Write pages

  • / is the home page. Hunchwire adds its script to every HTML page.
  • Put a form where you want it with <div data-hunchwire="waitlist"></div> or <div data-hunchwire="login"></div>. See The embed for what each does and its options. You don't add the script tag: hosted pages load it themselves.
  • Count clicks on any link or button with data-hunchwire-track="plan-team".
  • Count something that isn't a click from your page's own script: window.hunchwire?.track("name-typed"). Use it for typing in a field or a swipe. It's recorded as a tracked click with that name, up to 100 characters.
  • Hunchwire doesn't count visits, engagement or clicks from bots, automated browsers, the workspace's ignored IP addresses, or browsers opted out with ?hunchwire=ignore. See Internal traffic.
  • Every page also reports how long it was visible and how far it was scrolled. Hunchwire records each visit's device, browser and country. There's nothing to set up.
  • To share one stylesheet, add a /styles.css page and link to it. Scripts work the same way, with /app.js.
  • Real product photos or renders sell a product better than drawings. Upload them as images.
  • A site that looks like a real product, with home, pricing, sign-up and log-in pages, tests demand far better than a lone form.
  • Search engines: nothing to set up.
    • Each site serves /robots.txt and /sitemap.xml.
    • The sitemap lists every HTML page except /404 and pages with <meta name="robots" content="noindex">. Add that tag to a page that shouldn't be found, like a fake login.
    • Every page with a <head> gets a <link rel="canonical"> to the site's main address, unless it has its own. That address is your connected domain when you have one, else <slug>.hunchwire.site. For Hunchwire's own site, it's hunchwire.com.
    • /Pricing and /pricing/ redirect (301) to /pricing, keeping the query string. Link pages by their lowercase path, with no trailing slash.
    • Hunchwire caches .css, .js, .svg, .txt, .md and image files, and Markdown copies, for 5 minutes. An edit can take that long to show.
  • AI assistants and agents: nothing to set up.
    • Every page in the sitemap also has a Markdown copy of its text at its path plus .md: /pricing.md, and /index.md for /. The copy names the page as canonical.
    • To write the copy yourself, upload a page at that path.
    • To give assistants a summary of the site, add an /llms.txt page in the llms.txt format: a # Name, a > one-line summary, then links to the pages, ideally their .md copies.

Be honest with visitors. Forms must reveal that the product isn't available yet. Pages must not invent customers, testimonials or statistics.

Versions

Test several pitches at once. Each visitor gets one version of the hosted site, picked at random by weight, and keeps it for 90 days through a cookie on the site. Search engines and bots always see the first live version. Versions work only with hosted_pages.

A version has:

  • key: A, B… in the order added. Keys are never reused.

  • name: its name.

  • weight: its share of new visitors, relative to the other live versions.

  • status: live, paused (keeps its visitors but gets no new ones) or ended.

  • pages: how many HTML pages it has.

  • preview_url: a link that shows this version as visitors see it. null for an ended version.

GET /projects/:id/versions Needs analytics

List the versions.

Details
  • Returns versions, oldest first, and split_runs: true while more than one version is live or paused.
  • An empty list means one version: everyone sees the same pages.
GET /projects/:id/versions
curl https://app.hunchwire.com/projects/:id/versions \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

POST /projects/:id/versions Needs manage

Add a version.

Details
  • It copies the first live version's HTML pages once. The first time, the project's pages become A "Original" and the copy becomes B. From then on, the versions are independent.
  • Then change only what differs: PATCH /projects/:id/pages/:page_id on the new version's pages, which GET /projects/:id/setup lists.
  • At most 4 versions can be live or paused at once. Adding one sets the split to even.
POST /projects/:id/versions
curl -X POST https://app.hunchwire.com/projects/:id/versions \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"version": {"name": "Founder story"}}'

PATCH /projects/:id/versions/:version_id Needs manage

Change a version.

Details
  • version_id is the version's key, like B.
  • Rename it: {"version": {"name": "…"}}.
  • Pause or resume it: "status": "paused" or "live".
  • Set the split: {"split": "even"}, or {"weights": {"A": 70, "B": 30}} for the live versions. Weights add up to 100.
PATCH /projects/:id/versions/:version_id
curl -X PATCH https://app.hunchwire.com/projects/:id/versions/:version_id \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

DELETE /projects/:id/versions/:version_id Needs manage

End a version and delete its pages.

Details
  • Its results stay in Analytics. You can't delete the last version.
  • Live campaigns whose ads lead to a page only this version has are paused on Meta first. Returns versions and paused_campaigns.
DELETE /projects/:id/versions/:version_id
curl -X DELETE https://app.hunchwire.com/projects/:id/versions/:version_id \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

POST /projects/:id/versions/:version_id/keep Needs manage

End the test and keep this version.

Details
  • Everyone sees this version from now on. The others end, and their pages are kept.
  • Only do this when the founder decides. Hunchwire never picks a winner.
POST /projects/:id/versions/:version_id/keep
curl -X POST https://app.hunchwire.com/projects/:id/versions/:version_id/keep \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

Preview a version

Open a version's preview_url to see it as visitors do, for example https://loop.hunchwire.site/?version=C.

  • Add ?version= with a live or paused version's key to any page of the site.
  • Hunchwire records nothing: no visit, no clicks and no sign-up. The form shows, but takes no sign-up.
  • The preview never sets or changes the version you get on the site. Search engines don't index it.
  • Links between the site's pages keep ?version=, so you can click through the version.
  • An unknown or ended key shows the site as usual.

Send the founder the link when you change a version's pages, so they can check it.

See results by version in Analytics with filter[version][]=B, or use by=version to see the funnels side by side.

Your domain

Use the founder's own domain for two things: to show a hosted project's pages there, with a certificate (hosted_pages only), and to email the project's people from it (any project; see Email). A project on its own site (own_site_forms) adds its domain for email only.

Adding a domain works on any plan. Connecting it for pages, and sending email, need a paid plan.

Each project has one domain. For pages, a bare domain (tryloop.app) or its www adds both, and another subdomain (go.tryloop.app) covers only itself. For email, Hunchwire uses the domain as typed.

A site has one address: its primary host. That's the bare domain once it serves, else its www, else the free address. Every other address of the site answers a GET with a 301 to the same path and query there: the other half of the domain, and <slug>.hunchwire.site. A POST stays where it lands. Ask the founder to add the records for both halves, so both open.

GET /projects/:id/domain Needs analytics

Get the domain and the DNS records it needs.

Show 12 details
  • domain: the domain added, or null.
  • primary_host: the site's one address. Every other address of the site redirects there.
  • on: false until Hunchwire turns on custom domains.
  • connected: whether the domain is connected for pages.
  • hostnames: each has hostname, status, connected, serving, checked_at, error and its own records. status is pending, active (ownership verified and certificate issued) or failed. serving means active and connected: pages show there.
  • redirects_to: the host a hostname redirects to, or null when it's the primary host or doesn't serve yet.
  • records: every DNS record the founder adds where they bought the domain, for pages and email together. Each has type (ALIAS, CNAME, TXT or MX), name (relative to the domain; @ is the bare domain), host, value, priority, hint, for (pages, ownership or email) and status (found or waiting).
  • priority: the MX priority, as its own number, since DNS forms ask for it apart from the mail server. null for other types. value is the mail server alone.
  • hint: a line for the founder when their provider may need another type, or null. The bare domain's ALIAS says: on Cloudflare, add a CNAME on @, DNS only (not proxied).
  • The email records are TXT hw1._domainkey (the signing key), MX and TXT on bounce (where bounces come back), and TXT _dmarc. A DMARC record the domain already has counts.
  • Many providers call the bare domain's ALIAS an ANAME or "CNAME flattening". If the founder's provider has neither, they forward the bare domain to www there.
  • email: the domain's email setup.
    • on: false until Hunchwire turns on email.
    • status: pending, verified, failed or paused, or null before the domain is added.
    • paused: too many bounces or complaints. Sending stops until Hunchwire resumes it.
    • ready: whether the project can email its people now.
    • blocker: why not (see Email).
    • setup: the first email setup step still missing, or null once it's done. It's no_domain, unverified, failed, paused, no_mail_from or no_sender. Unlike blocker, it ignores the plan.
    • by_hand: whether the founder sends by hand instead (see Send by hand). Always false once email is set up.
    • checked_at and error.
GET /projects/:id/domain
curl https://app.hunchwire.com/projects/:id/domain \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

PATCH /projects/:id/domain Needs manage

Add the domain.

Details
  • PUT works too.
  • The records come back at once. Give them to the founder, or add them yourself if you can reach their DNS.
  • A domain added before email was on gets its email records from this call, sent with no domain.
PATCH /projects/:id/domain
curl -X PATCH https://app.hunchwire.com/projects/:id/domain \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain": "tryloop.app"}'

POST /projects/:id/domain/check Needs manage

Look up the records now.

Details
  • Hunchwire also checks every 5 minutes for a week. After that, it checks one half of the domain once a day while the other half serves.
POST /projects/:id/domain/check
curl -X POST https://app.hunchwire.com/projects/:id/domain/check \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

POST /projects/:id/domain/connect Needs manage

Connect the domain for pages, once the founder agrees.

Details
  • It's a release gate. Without a plan that has the domain feature, Hunchwire answers 402 plan_required (see Plans and gates). It then connects the domain by itself once the founder upgrades.
POST /projects/:id/domain/connect
curl -X POST https://app.hunchwire.com/projects/:id/domain/connect \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

DELETE /projects/:id/domain Needs manage

Remove the domain.

Details
  • Pages stop showing there, and email stops coming from it, at once. A held connect is dropped.
  • Live campaigns whose ads lead to it are paused on Meta first. The response then adds paused_campaigns.
DELETE /projects/:id/domain
curl -X DELETE https://app.hunchwire.com/projects/:id/domain \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

Forward paths to your app

Send some paths on the connected domain to the founder's own app. Visitors stay on the founder's domain: tryloop.app/ shows the hosted pages, and tryloop.app/join comes from the app.

Forwarding works only on the connected domain of a project with hosted pages. On the primary host, every method forwards. On the other half of the domain, a GET redirects to the primary host first, and other methods forward. The free address never forwards: a GET there redirects to the primary host too.

Path Matches
/privacy /privacy only
/join* /join, /join/step-2 and /joined
/hb-assets/* everything under /hb-assets/

GET /projects/:id/forwarding Needs manage

Get what the project forwards.

Details
  • to: the app's address, or null when nothing is forwarded.
  • paths: the paths forwarded.
  • forwarding_secret_hint: the last 4 characters of the forwarding secret, or null.
  • hidden_pages: hosted pages a path hides. A forwarded path wins over a page.
GET /projects/:id/forwarding
curl https://app.hunchwire.com/projects/:id/forwarding \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"
Response
{"to": "https://app.tryloop.app", "paths": ["/join*", "/privacy"], "forwarding_secret_hint": "x3Fq", "hidden_pages": ["/privacy"]}

PATCH /projects/:id/forwarding Needs manage

Set the app's address and the paths.

Show 5 details
  • to is https and names a host, with nothing after it. It can't be a Hunchwire address, an IP address or a private network.
  • You can forward up to 20 paths. Each starts with /. End one with * to include everything after it.
  • Hunchwire's own paths can't be forwarded: /, /embed.js, /e/*, /robots.txt, /sitemap.xml, /share/*, /invite/*, /confirm/*, /unsubscribe/*, /up and /lab_mail/*.
  • The first save makes the forwarding secret. It comes back once, as forwarding_secret. Give it to the founder for the app's environment.
  • A 422 lists what's wrong in errors.
PATCH /projects/:id/forwarding
curl -X PATCH https://app.hunchwire.com/projects/:id/forwarding \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"forwarding": {"to": "https://app.tryloop.app", "paths": ["/join*", "/privacy"]}}'

POST /projects/:id/forwarding/secret Needs manage

Make a new forwarding secret. Only do this when the founder asks.

Details
  • It comes back once, as forwarding_secret. The old one stops working at once.
POST /projects/:id/forwarding/secret
curl -X POST https://app.hunchwire.com/projects/:id/forwarding/secret \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

DELETE /projects/:id/forwarding Needs manage

Stop forwarding.

Details
  • The paths show the hosted pages again, and the forwarding secret is gone.
DELETE /projects/:id/forwarding
curl -X DELETE https://app.hunchwire.com/projects/:id/forwarding \
  -H "Authorization: Bearer $HUNCHWIRE_TOKEN"

Hunchwire sends the visitor's method, path, query, body and cookies on to the app. It returns the app's answer as it is, redirects and cookies included. It never follows a redirect.

Hunchwire adds these headers:

Header Value
X-Forwarded-Host The domain the visitor used, such as tryloop.app.
X-Forwarded-Proto https.
X-Hunchwire-Client-Ip The visitor's IP address.
X-Hunchwire-Time-Zone The visitor's time zone, such as America/Toronto. Left out when it isn't known.
X-Hunchwire-Forwarding-Secret The forwarding secret. Trust the two headers above only when it matches.

Limits: the app has 5 seconds to connect and 15 to answer. A request body can be up to 1 MB, and an answer up to 10 MB.

Status Meaning
502 The app didn't answer in time, or answered with more than 10 MB.
413 The request body is over 1 MB.
503 Too many forwards at once. Retry after the seconds in Retry-After.

Forwarded pages count in Analytics only with the embed snippet (see The embed). On the same domain, a visitor who moves from a hosted page to a forwarded one is one visit.

Made from skill.md, the file agents read. Add .md to any page's address for its Markdown.