Provision a product
A provisioning script that is safe to run on every deploy: find or onboard, the hook, facts, goals, scheduled spend, and what each answer looks like.
For a team that onboards its own product from a script in its repo and runs that script again on every deploy. Every step below is safe to repeat.
Keys
Onboard with an agency key that can make clients (admin). Then give the product's agent its own key pinned to its client: create_api_key {name: "advernue-agent", clientId, role: "site_admin"}. A pinned key is refused every other client's data, so one product's agent cannot read another's.
The script, step by step
Find or onboard.
list_clients {domain: "advernue.example"}returns the client whose site is on that domain (www. and the scheme are ignored), with itsdomains. When the list is empty,onboard_site {domain, framework, verification, trackerFrequency: "weekly"}. Callingonboard_siteagain for the same domain finds the same client, so either way is safe.Deploy hook.
create_deploy_hook {clientId, ifMissing: true}makes a hook only when there is none, and answers{created: false}without a URL otherwise. WithoutifMissing, it rotates the URL.get_deploy_hook {clientId}answersexists.Brief.
update_client_brief {clientId, add: [...]}adds an entry once: the same kind, title, text and address again returns the entry already there.Facts. Keep them in the repo as
/.well-known/seolyticize.json({"facts": [{"key": "starter plan price", "value": "$29/month", "category": "price"}]}) and callimport_client_facts {clientId, dryRun: true}, then withoutdryRun. Every deploy hook call compares the published file with the registered facts and lists differences asfactsDrift.Goals and funnels.
create_goal {..., upsertBy: "name"}andcreate_funnel {..., upsertBy: "name"}return the existing one when it is defined the same way (created: false) and replace it when it is not.list_goalsandlist_funnelslist them.Scheduled spend.
get_scheduled_work {clientId}lists every paid job on a schedule with the call that pauses it. A new client has none: the domain sweep is off untilset_domain_sweepturns it on, and the tracker bills only once it has keywords.
Dry runs
dryRun: true is taken by paid tools (it quotes and buys nothing) and by the writes that say so (set_client_facts, import_client_facts). Any other tool refuses it as an argument it does not take, rather than running for real.
Hosts without DNS
On a host you cannot add DNS records to (an sslip.io or preview host), use verification: "file" or "meta". verify_site then adds a URL-prefix Search Console property (https://host/), because a Domain property (sc-domain:) needs a TXT record. When the product gets its own domain, move_site keeps the client, its history and site key, and returns the new domain's manifest. Google's Change of Address has no API, so a person starts it in Search Console; until then the old property's history stays on the old property.
Signed-in apps
Analytics. The standard tag (
a.js) sends the page's origin and path, never its fragment, and nothing of its query string bututm_*campaign tags, so?ref=and tokens in the query never leave the page. A token in the path is the remaining risk: addexcludePathsfor the signed-in area (enforced on the server too), andpathTemplatesfor pages named after a customer.Crawling. Keep the crawler out of the signed-in area with
set_crawl_settings {clientId, excludePatterns: ["/dashboard/*", "/settings/*"]}: excluded pages are not fetched, so their redirects to the sign-in page are not reported.accept_issuemarks a finding as intended. A new site onboarded withframework: "other"renders app shells before auditing them (render: "shells").Conversions after the visit. A sign-up that finishes on another origin, or an invoice paid days later, is credited through the server collector: send the landing pageview from your server with the server key (
issue_server_collect_key), keep the visit token it answers with on the account, and send the conversion later withvisit. A token is never given to the browser, so nothing about a visit is stored on the visitor's device.
What the answers look like
onboard_site (shortened). files[].content is served exactly at files[].path:
{
"clientId": "client_1a2b3c4d",
"domain": "advernue.example",
"created": true,
"files": [{ "path": "/1f2e3d.txt", "content": "1f2e3d", "purpose": "IndexNow key: …" }],
"head": [],
"analytics": {
"siteKey": "sk_example123",
"snippet": "<script defer data-site=\"sk_example123\" src=\"https://seolyticize.com/a.js\"></script>",
"install": "…",
"suggestedExclude": "/api/*,/admin/*,/account/*,/dashboard/*,/login,/verify,/reset"
},
"csp": { "script-src": ["https://seolyticize.com"], "connect-src": ["https://seolyticize.com"] },
"crawl": { "render": "shells", "why": "…" },
"scheduledPaidWork": [],
"next": "Deploy the files above … then call verify_site {clientId: \"client_1a2b3c4d\"}."
}get_client: client (with domainSweep), projects[].sites, trackers (each with frequency) and analytics: {siteKey, collectUrl} at the top level. Nothing in it is secret.
Shown once, and only in the answer that makes them: the deploy hook url (create_deploy_hook), an API key, the server collect key, and a portal link.