Open the app →

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)

  1. Sign in at https://golem.to/app — email, magic link, no password.
  2. In Forms, write your form. Under “Where will you share it?”, tick each place — every tick becomes a tracked link pointing at the form.
  3. 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.
  4. Watch Overview: every response shows which link brought it.
  5. 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

  1. Call whoami first. Every tool is scoped to the one team this credential acts for; a key cannot switch teams.
  2. Create the form (create_form): title, fields, published: true. Keep fields few; every field type carries its own validation and autocomplete.
  3. Create one link per place (create_link) with destination_url = the form's public_url and a channel naming 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.
  4. Read results: analytics_overview for the roll-up, compare_sources for "which link won?", form_analytics / link_analytics for one funnel, analytics_map for where clicks came from.
  5. Mint the proof: create_report returns a public URL to hand to a client or sponsor; append .json for the machine-readable twin. revoke_report kills it for everyone, immediately.

Semantics that matter

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.

emailstring · requiredWhere 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.

tokenstring · requiredThe 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.

labelstringWhat this key is for; shown in the list.
expires_in_daysint · 1–365Days 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
archivedtrue · false · allWhich 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_urlstring · requiredWhere the short link sends people.
slugstringThe path after the host. Generated when omitted.
titlestringA name for your dashboard, not shown to visitors.
channelstringThe place this link lives: "newsletter", "linkedin", "flyer". This is what source comparison groups by.
domain_iduuidServe from a branded domain instead of the primary host.
expires_atRFC 3339When 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_urlstringNew destination; the short URL does not change.
slugstringRenames the public path. The old slug stops resolving.
titlestringDashboard name.
channelstringReclassifies the link's place.
enabledboolfalse pauses the redirect without retiring it.
expires_atRFC 3339 · nullnull 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
daysint · default 7Reporting 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
archivedtrue · false · allWhich slice: active (default), retired, or both.
POST/api/formscreate a form

A form is a titled list of fields, published at /f/<slug>.

titlestringShown to respondents.
fieldsarrayField objects, in order — see below.
publishedbooltrue makes the public URL live. Default: draft.
slugstringPublic path. Generated when omitted.
descriptionstringIntro text under the title.
confirmation_messagestringShown after submitting.
redirect_urlstringSend respondents somewhere after submitting instead.
logo_url · cover_image_urlstringThe form's own branding.
notify_emailstringEmails each response as it arrives.
domain_iduuidServe from a branded domain.

Each field

field_typestring · requiredOne of GET /api/form-field-types. Semantic types (email, phone, date, country...) carry their own validation.
labelstringThe question.
keystringStable machine key. Derived from the label when omitted.
requiredboolRefuse submission without it.
optionsstring[]For choice fields.
help · placeholderstringHint under the label · ghost text in the input.
hidden_valuestringFor 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
daysint · default 7Reporting 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
daysint · default 7Reporting window. Lifetime totals come separately, under all_time.
scopeall · links · formsWhich 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
daysint · default 7Reporting window.
form_iduuidOnly 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
daysint · default 7Reporting window.
scopeall · links · formsWhich 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
daysint · default 7Reporting window.
scopeall · links · formsWhich 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
daysint · default 7Reporting window.
scopeall · links · formsWhich 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_iduuid · requiredThe form whose funnel the report shows.
link_iduuidNarrow to one link's journeys.
titlestringThe heading the reader sees.
daysintDays 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
hostnamestring · requirede.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.