Docs
Golem does one job: it connects every response to the link that
brought it. This page covers running it yourself and handing it to an AI
assistant. Machines: fetch /llms.txt for
the plain-text index of everything here.
Quick start (five minutes)
- Sign in at https://golem.to/app — email, magic link, no password.
- In Forms, write your form. Under “Where will you share it?”, tick each place — every tick becomes a tracked link pointing at the form.
- Share each link in its place: the LinkedIn link on LinkedIn, the newsletter link in the newsletter. Every link has a permanent QR code for anything printed.
- Watch Overview: every response shows which link brought it.
- On the form’s row, Share report mints a public page of the results to send to a client or sponsor.
Connect an AI assistant (MCP)
Golem speaks MCP at POST https://golem.to/mcp. In claude.ai or Claude
Desktop: add a custom connector with that URL — Claude opens Golem's sign-in,
you approve, done. From a terminal:
claude mcp add --transport http golem https://golem.to/mcp
Headless agents skip OAuth and send an API key instead:
--header "Authorization: Bearer golem_key_...". Keys are minted in
the app's API tab, can carry an expiry, and revoking one
disconnects whatever holds it — OAuth connections included (they appear as
keys labelled “OAuth: …”).
Agent guide — operating Golem well
Everything the dashboard can do, the API and MCP can do. This is the path an assistant should take, and the semantics it must respect.
The golden path
- Call
whoamifirst. Every tool is scoped to the one team this credential acts for; a key cannot switch teams. - Create the form (
create_form): title, fields,published: true. Keep fields few; every field type carries its own validation and autocomplete. - Create one link per place (
create_link) withdestination_url= the form'spublic_urland achannelnaming the place ("newsletter", "linkedin", "flyer"). This join is the whole product: links not pointing at a form still count clicks, but responses cannot be traced to them. - Read results:
analytics_overviewfor the roll-up,compare_sourcesfor "which link won?",form_analytics/link_analyticsfor one funnel,analytics_mapfor where clicks came from. - Mint the proof:
create_reportreturns a public URL to hand to a client or sponsor; append.jsonfor the machine-readable twin.revoke_reportkills it for everyone, immediately.
Semantics that matter
- Update semantics: omitted field = keep current value; empty
string = clear it (
redirect_url,logo_url,cover_image_url,notify_email). - Nothing is deleted. Retiring (
archive_link/archive_form) makes the URL answer 410 while keeping every recorded event;restore_*undoes it. Prefer this over wishing for delete. - Rates carry their parts.
click_to_responsedivides responses attributed to a link by link opens that landed on a form;view_to_responsedivides responses by human form views. A zero denominator means "nothing to measure", not 0%. - Bots are excluded. Requests are classified
filtered_open(human),agent,bot,preview,scanner; totals count only the first. SendX-Golem-Agent: your-nameon anything you fetch or submit so your own traffic is classifiedagent. - Errors are JSON
{"error": "..."}with honest status codes; 410 means "existed, deliberately stopped". MCP tool errors come back asresult.isError, not JSON-RPC errors. - Rate limits: sign-in links 10/hour per client; form submissions are per-minute limited. Space out retries.
Machine-readable forms
Every form describes itself. GET https://golem.to/f/<slug> with
Accept: application/json (or /f/<slug>/schema)
returns stable field keys, value types, and submit instructions — including a
worked example. Submit answers as JSON to the same URL. Choice answers store
canonical values, so a form answered in three languages is one dataset.
Authentication
curl https://golem.to/api/analytics/overview \ -H "Authorization: Bearer golem_key_..."
One key authenticates REST and MCP. Keys belong to the team, are
capped by the role of whoever minted them, and optionally expire
(expires_in_days 1–365 at creation). Humans sign in by magic
link; sessions ride a cookie or Bearer session token.
REST reference
Same nouns as the MCP tools, same team scope as the credential calling them.
Everything speaks JSON and authenticates with
Authorization: Bearer golem_key_... (or a signed-in session)
unless marked public. Errors are {"error": "..."} with honest
status codes. Update semantics everywhere: an omitted field keeps its value;
an empty string (or null) clears it. Open an endpoint for its
parameters.
Sign in and sessions
POST/api/auth/magic-linkemail a sign-in link
Human sign-in. Emails a one-time link; redeeming it creates a session. No password exists. Rate limited to 10 requests per hour per client. Public.
| string · required | Where the link is sent. |
Returns
{"ok": true, "message": "..."} — and in local development with
no email provider configured, a login_url to open directly.
curl https://golem.to/api/auth/magic-link \
-H "Content-Type: application/json" \
-d '{"email": "you@company.com"}'
POST/api/auth/verifyredeem the emailed token
Exchanges the token from the sign-in email for a session. Public.
| token | string · required | The golem_login_... token from the emailed link. |
Returns
token (a session token to send as Authorization:
Bearer ...), user_id, email.
GET/api/auth/mewho this credential is
The user (or key) behind the request, the team it acts for, and its role. Call it first: every other endpoint is scoped to this one team.
POST/api/auth/logoutend the session
Invalidates the current session token. API keys are revoked via
DELETE /api/keys/<id>, not here.
API keys
GET/api/keyslist keys
Every key the team holds — label, creation, expiry — never the secret,
which is shown once at creation. OAuth-connected assistants appear here too,
labelled OAuth: ...; revoking the key disconnects them.
POST/api/keysmint a key
Creates a team-scoped key that authenticates both /api/* and
POST /mcp. Its authority is capped by the role of whoever mints
it. Requires a session — a key cannot mint keys, because a key that can mint
keys defeats revocation.
| label | string | What this key is for; shown in the list. |
| expires_in_days | int · 1–365 | Days until the key stops working. Omitted: lives until revoked. |
Returns
201 with the golem_key_... token. Store it now;
it is not shown again.
curl https://golem.to/api/keys \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{"label": "reporting script", "expires_in_days": 90}'
DELETE/api/keys/<id>revoke a key
Immediate. Whatever holds the key — script, assistant, OAuth connection — loses access on its next request.
Links
GET/api/linkslist links
| archived | true · false · all | Which slice: active (default), retired, or both. |
Returns
Each link with its stats, short_url, and qr_url.
POST/api/linkscreate a link
One link per place you promote. Point destination_url at a
Golem form's public URL and every response traces back to this link; point it
anywhere else and it still counts clicks.
| destination_url | string · required | Where the short link sends people. |
| slug | string | The path after the host. Generated when omitted. |
| title | string | A name for your dashboard, not shown to visitors. |
| channel | string | The place this link lives: "newsletter", "linkedin", "flyer". This is what source comparison groups by. |
| domain_id | uuid | Serve from a branded domain instead of the primary host. |
| expires_at | RFC 3339 | When the link stops redirecting. |
Returns
201 with the link, its short_url, and a permanent
qr_url.
curl https://golem.to/api/links \
-H "Authorization: Bearer golem_key_..." \
-H "Content-Type: application/json" \
-d '{"destination_url": "https://golem.to/f/reg", "slug": "launch", "channel": "newsletter"}'
GET/api/links/<id>read one link
The link with stats, short_url, and qr_url.
PATCH/api/links/<id>update a link
Send only what changes. Editing the destination keeps the slug and the QR code — reprint nothing.
| destination_url | string | New destination; the short URL does not change. |
| slug | string | Renames the public path. The old slug stops resolving. |
| title | string | Dashboard name. |
| channel | string | Reclassifies the link's place. |
| enabled | bool | false pauses the redirect without retiring it. |
| expires_at | RFC 3339 · null | null removes an expiry. |
POST/api/links/<id>/archiveretire (410)
The URL answers 410 Gone; every recorded event is kept and the
slug stays reserved. Nothing in Golem deletes. POST
/api/links/<id>/restore undoes it.
GET/api/links/<id>/qr.pngpermanent QR · public
The link's QR code as a PNG. Public and permanent — print it; editing the destination later does not invalidate it. Refused once the link is retired, for the same reason the URL is.
GET/api/links/<id>/analyticsthe link's funnel
| days | int · default 7 | Reporting window. |
Returns
Opens → form views → starts → responses for journeys that began at this link, with rates and their numerators and denominators.
Forms
GET/api/formslist forms
| archived | true · false · all | Which slice: active (default), retired, or both. |
POST/api/formscreate a form
A form is a titled list of fields, published at /f/<slug>.
| title | string | Shown to respondents. |
| fields | array | Field objects, in order — see below. |
| published | bool | true makes the public URL live. Default: draft. |
| slug | string | Public path. Generated when omitted. |
| description | string | Intro text under the title. |
| confirmation_message | string | Shown after submitting. |
| redirect_url | string | Send respondents somewhere after submitting instead. |
| logo_url · cover_image_url | string | The form's own branding. |
| notify_email | string | Emails each response as it arrives. |
| domain_id | uuid | Serve from a branded domain. |
Each field
| field_type | string · required | One of GET /api/form-field-types. Semantic types (email, phone, date, country...) carry their own validation. |
| label | string | The question. |
| key | string | Stable machine key. Derived from the label when omitted. |
| required | bool | Refuse submission without it. |
| options | string[] | For choice fields. |
| help · placeholder | string | Hint under the label · ghost text in the input. |
| hidden_value | string | For hidden fields: the value submitted. |
curl https://golem.to/api/forms \
-H "Authorization: Bearer golem_key_..." \
-H "Content-Type: application/json" \
-d '{"title": "Early access", "published": true, "fields": [
{"field_type": "email", "label": "Work email", "required": true},
{"field_type": "single_choice", "label": "Team size",
"options": ["Just me", "2-10", "11+"]}]}'
GET/api/forms/<id>read one form
The form with its fields and public URL. The respondent-facing
contract lives at /f/<slug>/schema, which needs no auth.
PATCH/api/forms/<id>update a form
Same fields as create; send only what changes. An empty string clears
redirect_url, logo_url, cover_image_url,
or notify_email. domain_id moves the form:
null back to the primary host, an id onto that (active) branded
domain. Sending fields replaces the field list — include
id on fields you are keeping, so their answers stay attached.
POST/api/forms/<id>/archiveretire (410)
The public form answers 410 Gone; every response is kept.
POST /api/forms/<id>/restore undoes it.
GET/api/form-field-typesthe field palette
The fourteen field types with their keys and what each validates. The form builder in the app is generated from this same list.
Responses and translations
GET/api/forms/<id>/submissionsevery response
Responses with canonical answer values (a form answered in three languages
is one dataset), arrival time, attribution to the link that brought each one,
and location where known. /submissions.csv is the same data as a
CSV export.
GET/api/forms/<id>/analyticsthe form's funnel
| days | int · default 7 | Reporting window. |
Returns
Views → starts → responses, total and attributed submissions, and click-to-response computed only over clicks that could convert.
GET/api/forms/<id>/field-summaryanswers per option
How many respondents picked each option, per choice field — the "what did people answer" view without downloading the CSV.
GET/api/forms/<id>/locationswhere responses came from
Countries and cities of the responses (not the traffic), plus a
location object stating what the geolocation backend can actually
know right now — so an empty list can say "no database loaded" instead of
reading as "nobody, from nowhere".
GET/api/forms/<id>/translationslist translations
The form's locales. PUT
/api/forms/<id>/translations/<locale> adds or replaces one
(labels, options, messages in that language); DELETE removes it.
Respondents get the best match from ?lang=, then
Accept-Language, then the default. Answers always store the
canonical value.
Analytics
GET/api/analytics/overviewthe team roll-up
| days | int · default 7 | Reporting window. Lifetime totals come separately, under all_time. |
| scope | all · links · forms | Which half of the product to report. Default: all. |
Returns
Human opens, form views, starts, and responses for the period with change
against the previous equal period; attributed_submissions and
attributable_opens — the numerator and denominator of
click_to_response_rate — reported alongside it, so "nobody
converted" and "no link points at a form yet" stay distinguishable; and the
top sources.
curl "https://golem.to/api/analytics/overview?days=30" \ -H "Authorization: Bearer golem_key_..."
GET/api/analytics/sourceswhich link won
| days | int · default 7 | Reporting window. |
| form_id | uuid | Only journeys that ended at this form. |
Returns
Per link: real filtered opens, responses produced, and the rate between them — not the share of attributed journeys, which would read 100% for every link that produced anything.
GET/api/analytics/timeseriesdaily activity
| days | int · default 7 | Reporting window. |
| scope | all · links · forms | Which half to chart. |
Returns
Daily buckets, gap-filled — a day with nothing is a zero, so a chart cannot imply continuity that was not there.
GET/api/analytics/breakdownwho and from where
| days | int · default 7 | Reporting window. |
| scope | all · links · forms | Which half to break down. |
Returns
Request classification (human, agent, bot, preview, scanner), top referring
hosts, channels, countries and cities, plus the location
capability object.
GET/api/analytics/mapclustered coordinates
| days | int · default 7 | Reporting window. |
| scope | all · links · forms | Which half to map. |
Returns
Located clicks as city-centroid coordinates with counts, plus
located and total so a map of three dots is never
mistaken for the whole traffic.
Shared reports
POST/api/reportsmint a public report
Creates a revocable public page of one funnel — the thing you send a client or sponsor. The reporting window freezes at creation.
| form_id | uuid · required | The form whose funnel the report shows. |
| link_id | uuid | Narrow to one link's journeys. |
| title | string | The heading the reader sees. |
| days | int | Days back from now. |
Returns
201 with the public URL (/r/<token>).
Append .json for the machine twin — every rate ships with its
numerator and denominator.
curl https://golem.to/api/reports \
-H "Authorization: Bearer golem_key_..." \
-H "Content-Type: application/json" \
-d '{"form_id": "...", "title": "March campaign", "days": 30}'
GET/api/reportslist reports
Every report the team has minted, with its URL and whether it is still live.
POST/api/reports/<id>/revokewithdraw it
The public URL stops resolving for everyone, immediately. Idempotent.
Branded domains
GET/api/domainslist domains
The team's branded hostnames and their verification state.
POST/api/domainsadd a domain
| hostname | string · required | e.g. go.company.com — a subdomain, since DNS cannot put a CNAME on an apex; apexes are refused. Golem serves only links and forms on it. |
Returns
The two records to add: a CNAME for routing, and a TXT record
(_golem.<hostname>) proving you control the DNS. The domain
activates when a check sees both — a pointing CNAME alone is not proof of
ownership.
POST/api/domains/<id>/checkdiagnose DNS
Looks up the records as the world sees them and answers expected vs found,
with concrete fix text when they disagree. Activation requires both the CNAME
and the ownership TXT to verify. GET /api/domains/<id> reads
the stored state without re-checking.
POST/api/domains/<id>/removeremove a domain
Nothing serves on the hostname any more and it becomes claimable again —
removal is the one transition that releases a name rather than
reserving it. Refused while unarchived links or forms still live on the
domain: archive them, or move the forms first (PATCH
/api/forms/<id> with domain_id). Recorded events are
kept. Idempotent.
Shared reports
https://golem.to/r/<token> is a public funnel report — no account
needed to read it. Append .json for the machine twin: every rate
ships with its numerator and denominator, the methodology block states that
bots are excluded, and located clicks come as coordinates. Zoom the map with
scroll or pinch. Live sample: https://golem.to/r/sample.
Health
GET https://golem.to/healthz answers liveness and whether the geolocation
database is loaded — the map stays honest by saying when it cannot know.
The deepest reference is the MCP tool catalog itself — connect and list the tools; every description says what the tool does, what it returns, and what it deliberately does not do.