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
- The console's Events screen shows raw events per link and, beside them, the last rejects with the reason in plain words (unknown token, expired link, duplicate event id…).
GET /v1/events?ref=…returns the same raw rows over the API.- Every error response is
{"error": {"code": "…", "message": "…"}}; the codes are listed in the API reference.