Getting started

Everything below works with curl. You need a project and its two keys; then you register a link, put its token in an outbound URL, tag the landing page, and read the metrics.

Keys

Key Header Used by Grants
server key sk_… Authorization: Bearer sk_… your backend links, metrics, raw events
client key pk_… X-Go-Key: pk_… the web tag, the mobile libraries sending events, resolving a token to its deep link

The server key is a secret: keep it in your backend's configuration. The client key ships inside pages and apps and only identifies the project; what gates events is the link token (unknown, expired or deleted tokens are rejected) plus rate limits.

Both come from the console: an admin of the project mints a server key under Settings (the secret is shown once), and the client key is on Setup, already inside a copy-paste tag. If you have no login yet, ask the operator who set up your project.

1. Register a link

One link per ad unit. ref is your own id; the same ref upserts the same link, so you can send your whole set on every publish. deep_link is what the app libraries hand to your app when a token resolves — url is the routing field, anything else rides along. If your backend already knows the app URL (it used to fill af_dp or deep_link=), send it here and it is stored as given. metadata is a flat object of scalars you can group metrics by.

{
  "links": [
    {
      "ref": "job-123-meta-ad-9",
      "destination_url": "https://example.com/job/123",
      "deep_link": { "url": "myapp://job/123", "screen": "job" },
      "metadata": { "campaign": "autumn", "platform": "meta" }
    }
  ]
}

If your backend only has web URLs, do not send deep_link at all. Under Settings → Deep links in the console, an admin writes rules once — a pattern over the destination URL and a template — and every link registered without a deep_link gets one derived from its destination_url:

[
  { "match": "^https://example\\.com/job/(?P<id>\\d+)", "deep_link": { "url": "myapp://job/${id}" } },
  { "match": ".*", "deep_link": { "url": "${url}" } }
]

The tester on that screen shows what a URL would get; "Reapply to existing links" brings older links up to date (an ad already published keeps its old URLs until you re-upsert the link). The last rule above makes the web URL itself the deep link, which is right when your app opens Universal / App Links. Both ways are first-class; the result below looks the same.

curl -X POST https://go.roiguru.net/v1/links \
     -H "Authorization: Bearer sk_…" -H "Content-Type: application/json" \
     -d @links.json

The response answers for each link in request order with everything the ad needs: its id and token, the tracked_url for the ad's web destination, and the deep link as stored — sent by you or derived by your rules (deep_link_source) — plus tracked_deep_link for the ad's deep-link field. You never build a URL yourself:

{
  "links": [
    {
      "ref": "job-123-meta-ad-9",
      "id": "lnk_01J…",
      "token": "7Kq9mZ2pX4vL8nR3tW6y",
      "click_param": "gol=7Kq9mZ2pX4vL8nR3tW6y",
      "tracked_url": "https://example.com/job/123?gol=7Kq9mZ2pX4vL8nR3tW6y",
      "deep_link": { "url": "myapp://job/123", "screen": "job" },
      "deep_link_source": "explicit",
      "tracked_deep_link": "myapp://job/123?gol=7Kq9mZ2pX4vL8nR3tW6y",
      "status": "active",
      "expires_at": "2026-12-16T10:15:02Z",
      "created": true
    },
    {
      "ref": "job-124-meta-ad-9",
      "error": { "code": "invalid_request", "field": "destination_url", "message": "must be an absolute http(s) URL" }
    }
  ],
  "failed": 1
}

Up to 500 links per call. Each link answers for itself: one that fails validation carries error naming the field and is not written, the others are. A 400 invalid_request for the whole call happens only when the envelope is wrong — not JSON, links empty or over 500, or the same ref twice in one batch.

2. Put the token in the outbound URL

Use tracked_url as the ad's web destination: it is your destination_url with gol=<token> added (a gol already on it is replaced; the query and fragment are kept):

https://example.com/job/123?gol=7Kq9mZ2pX4vL8nR3tW6y

Use tracked_deep_link as the ad's deep-link field (Meta app link, Google app URL): it is your deep_link.url with gol=<token> added the same way, whatever the scheme, so an open of the already-installed app carries the token too. It is null when the link has no deep_link.url. Google accepts only https app links there — the ${url} rule above gives you one. click_param remains for a URL go never saw.

Verify a key

GET /v1/me writes nothing and answers with the key's project. With a server key it also returns the key's id and label and the batch and rate limits, so a "test connection" button in your backend has something harmless to call:

curl https://go.roiguru.net/v1/me -H "Authorization: Bearer sk_…"

When an ad stops

Tell go, and the link expires after the project's post-close window (30 days by default — long enough for the clicks already made to turn into installs):

curl -X POST https://go.roiguru.net/v1/links/close \
     -H "Authorization: Bearer sk_…" -H "Content-Type: application/json" \
     -d '{"refs": ["job-123-meta-ad-9"]}'

Publishing the same ref again reopens the link with the same token. There is nothing to keep alive: links only expire on their own after the project's TTL (400 days), a safety net for the ones nobody closed.

3. Tag the landing page

<script src="https://go.roiguru.net/s.js" data-key="pk_…"></script>

The tag reads gol from the URL, stores it first-party for 30 days, sends a land event, decorates your store links (<a data-go-store="android">) so the Play Install Referrer carries the token, and reports store_click. Details and the npm form: Web tag. The mobile libraries, when they ship, send install and open with the same token.

4. Read metrics

Rollups are hourly and served per day in the timezone you ask for. Two ways in:

Keep a copy in sync — the change feed. Every day row that changed since your last cursor, with its current totals and the link's ref and metadata on the row:

curl "https://go.roiguru.net/v1/metrics/changes?tz=Europe/Istanbul" \
     -H "Authorization: Bearer sk_…"

Upsert each row by (link_id, day, type, name, platform), then store next_since and pass it as since next time; keep calling while more is true. Late events simply show up as changed rows — no windows to guess, nothing to diff. Changes appear about 30 seconds after the events arrive.

Ask a question — the query. Group by link, by day, or by any metadata key (metadata.campaign), over a date range of at most 93 days:

curl "https://go.roiguru.net/v1/metrics?from=2026-09-01&to=2026-09-18&tz=Europe/Istanbul&group_by=link&type=land,install,open" \
     -H "Authorization: Bearer sk_…"

In both, a row for a day that is still receiving events is marked partial.

When nothing arrives