# API docs --- # Introduction Seolyticize is an SEO platform for agencies: rank tracking, site audits, backlinks, keyword research, Search Console and GA4, AI visibility and client reports, for every client an agency manages. Everything the app does is built from a catalogue of **tools** (about 500 of them), and the API is that same catalogue, reachable from your own code and from AI assistants. ## One catalogue, four ways in Every tool can be called four ways. A key that may call a tool in one of them may call it in all of them, and nowhere else. | Surface | Use it for | | --- | --- | | REST | Your own code and scripts: `POST /api/v1/tools/{name}` | | MCP | AI assistants such as Claude Code, Claude Desktop and Cursor: `/api/mcp` | | TypeScript SDK | Typed calls from Node, Deno, Bun or the browser | | Command line | The `seolyticize` command, for terminals and CI | ## Base URL All examples use the production host: | Purpose | URL | | --- | --- | | Tool calls | `https://seolyticize.com/api/v1/tools/{name}` | | Tool discovery for your key | `https://seolyticize.com/api/v1/tools` | | OpenAPI 3.1 document | `https://seolyticize.com/api/v1/openapi.json` | | MCP server | `https://seolyticize.com/api/mcp` | Requests and responses are JSON. The OpenAPI document is public and needs no key, so you can read what the API does before you have an account. Every operation in it is also listed under **API reference** in the sidebar. ## The objects you work with **Agency.** Your account. Every client, key, report and charge belongs to exactly one agency, and data never crosses between agencies. **Client.** A website you do SEO for, with a name and a domain. Most tools address one client and take a `clientId`; the rest work across the agency (`list_clients`) or on any domain at all (`get_domain_facts`). **Rank tracker.** A set of keywords captured for a client daily or weekly, on one search engine, location and language. Tools that read rankings take a `trackerId` as well as a `clientId`. **Key.** An API key belongs to the agency, carries a role, and is either agency-wide or pinned to one client. See [Authentication](./authentication). ## What a tool looks like A tool has a name (`list_clients`, `get_rank_summary`, `keyword_explorer`), an input described as JSON Schema, a permission it needs (`data:read`, `spend:incur` and so on), and a flag saying whether it addresses one client. You send its input as the JSON body: ```bash curl -X POST https://seolyticize.com/api/v1/tools/list_clients \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` and get the result back under `data`. [Calling tools](./calling-tools) covers the request and response in full. :::hint{style="info" title="Figures it does not have"} Seolyticize never prints a zero for a number it did not measure. Where data is missing — a domain nobody has crawled, a property that is not connected — a field is absent or `null`, not `0`. Treat a missing value as unknown, not as nothing. ::: ## Running Seolyticize from an agent If you are, or are building, an AI agent that should run an agency's SEO end to end — onboarding clients, auditing, reporting, setup — start with the [Agent playbooks](./start-here). Each step names the tool that does it. --- # Authentication Every call carries an API key as a bearer token: ```http Authorization: Bearer sk_seo_… ``` The same key works on REST, on the MCP server, in the SDK and in the command line. Assistants can also sign in through OAuth instead of using a key; see [MCP server](./mcp-server). ## Create a key Keys are made in the app under **Settings → API keys**, by an owner or admin of the agency. An existing key with the `admin` role can also create one with the `create_api_key` tool. The secret starts with `sk_seo_` and is **shown exactly once**. Only a hash of it is stored, so a lost key is replaced, not recovered. Keep it in an environment variable or a secret store, never in source code. ## Agency-wide or one client A key is either: - **agency-wide** — it can reach every client the agency has; or - **pinned to one client** — it cannot reach any other client's data, on any surface. A pinned key is what makes it safe to hand a key to an integration or an assistant that works for a single customer: whatever it is asked, it can only see that one client. ## Roles Each key carries a role, and the role decides which tools the key may call. A key can never be an owner. | Role | Can | | --- | --- | | `client_viewer` | Read only. Cannot write, spend or send anything. | | `analyst` | Read, write data and send reports. | | `manager` | The default. Also spends money (live lookups, crawls, submissions) and manages projects. | | `admin` | Also manages clients and API keys, and agency settings. | A key pinned to one client is capped at `manager`: admin powers are agency-wide by nature. Every tool declares the permission it needs, and the roles map onto them like this: | Permission | Lets a tool | `client_viewer` | `analyst` | `manager` | `admin` | | --- | --- | :---: | :---: | :---: | :---: | | `data:read` | read data | ✓ | ✓ | ✓ | ✓ | | `data:write` | change data (keyword lists, notes, rank trackers…) | | ✓ | ✓ | ✓ | | `report:send` | send reports | | ✓ | ✓ | ✓ | | `spend:incur` | make calls that cost money | | | ✓ | ✓ | | `cost:view` | read what the agency spent | | | ✓ | ✓ | | `project:manage` | manage projects | | | ✓ | ✓ | | `client:manage` | create, change and archive clients, and their alert rules | | | | ✓ | | `api:manage` | create and revoke API keys | | | | ✓ | | `agency:settings` | change agency settings | | | | ✓ | You never need to work this out by hand: discovery returns only the tools your key may call. ## What may this key do? `GET /api/v1/tools` answers with every tool the presented key may call, and the key's own reach: ```bash curl https://seolyticize.com/api/v1/tools \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" ``` ```json { "tools": [ { "name": "list_clients", "permission": "data:read", "…": "…" } ], "caller": { "role": "manager", "clientIds": "all" } } ``` `caller.clientIds` is `"all"` for an agency-wide key, or a list holding the one client a pinned key may reach. ## Revoking a key Revoke a key under **Settings → API keys**, or with `revoke_api_key`. A revoked key is refused on its very next request, including on an MCP connection that is already open: the key is checked on every request, not once per session. --- # Calling tools Every tool is one endpoint: ``` POST https://seolyticize.com/api/v1/tools/{name} ``` with your key as a bearer token and the tool's input as the JSON body. There are no other verbs and no path parameters: a client id, a domain or a date range is a field in the body. :::tabs ::tab{title="cURL"} ```bash curl -X POST https://seolyticize.com/api/v1/tools/get_rank_summary \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"clientId":"","trackerId":""}' ``` :: ::tab{title="TypeScript"} ```ts import { createClient } from "@seolyticize/sdk"; const seo = createClient({ apiKey: process.env.SEOLYTICIZE_API_KEY! }); const summary = await seo.tools.get_rank_summary({ clientId: "", trackerId: "", }); ``` :: ::tab{title="CLI"} ```bash seolyticize call get_rank_summary --client-id= --trackerId= ``` :: ::: ## The response A result comes back with status `200` under `data`: ```json { "data": { "…": "the tool's result" } } ``` A failure comes back under `error`, as one sentence written for the caller: ```json { "error": "That API key is not valid." } ``` Nothing internal is ever included: a request that could not be completed says only that, and the real reason stays in Seolyticize's own logs. [Errors and limits](./errors-and-limits) lists every status code. ## Input The body is the tool's input exactly as its JSON Schema describes it. Send `{}` for a tool that takes nothing. Fields you leave out take their defaults. A missing required field, or a value of the wrong type or outside its bounds, is a `400` whose message names the field. A field the schema does not declare is refused with `422` `invalid_input`, naming it and the fields the tool takes, so a misspelt argument is never silently dropped. Dates are strings in the format each schema states (`2026-01-01` or `20260101` — check the tool's schema). Ids such as `clientId` and `trackerId` come from other tools: `list_clients` for clients, `list_rank_trackers` for trackers. ## Discovery ```bash curl https://seolyticize.com/api/v1/tools \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" ``` `GET /api/v1/tools` lists the tools **your key** may call — each with its name, title, description, permission, whether it addresses one client (`clientScoped`), whether it only reads (`readOnly`), whether it can cost money (`spends`), and its input as JSON Schema — plus your key's role and reach. It is generated from the same registry that runs the calls, so build against it rather than against a copy of these pages: a tool added to the platform appears here the day it ships. ## OpenAPI ``` GET https://seolyticize.com/api/v1/openapi.json ``` The whole surface as OpenAPI 3.1, public and unauthenticated, for client generators, Postman and API gateways. It describes what exists, not what any one key may do. The **API reference** in the sidebar is generated from it. ## Same rules on every surface REST, MCP, the SDK and the command line all go through one registry. The tenancy check, the role check, input validation and error wording are the same whichever way a call arrives, so a call refused over REST is refused over MCP for the same reason. Every call is also recorded in the agency's activity trail with the surface it came from (`list_api_activity` reads it). --- # Errors and limits ## Status codes Every failure is a JSON body with one field, `error`, holding a sentence written for whoever made the call. Branch on the status code; show the sentence to a person. | Status | Meaning | Retry? | | --- | --- | --- | | `200` | Done. The result is under `data`. | — | | `400` | The body was not JSON, or did not match the tool's schema. The message names the field. | No: fix the input | | `401` | No key, or an unknown, revoked or expired one. | No: fix the key | | `403` | The key's role does not allow this tool, or the key may not reach that client. | No | | `404` | No tool by that name. | No | | `405` | Wrong method: tools take `POST`, discovery takes `GET`. | No | | `422` | A well-formed request the product declined — a duplicate name, a plan limit, a spending limit that would be passed — with the reason. | No, unless something changes | | `429` | Rate limited. Wait for `Retry-After`. | Yes, after the wait | | `500` | The request could not be completed. The reason is never included. | Reads: yes, with backoff. Writes: see below | :::hint{style="warning" title="Retrying writes"} A write that failed with a `500` may already have happened. Repeating `create_client` after a `500` can create two clients. Retry a `500` freely for tools that only read; for writes, check whether the change landed before trying again. A write whose connection dropped or timed out is safe to repeat when it carried an `Idempotency-Key` (below). The SDK follows exactly these rules. ::: ## Retrying writes: Idempotency-Key Every tool call accepts an `Idempotency-Key` header, up to 200 characters. Send one with a write, and a retry under the same key — after a dropped connection, a timeout, a crashed worker — is answered from the first request rather than run again: the client is created once, the report is sent once. ```bash curl -X POST https://seolyticize.com/api/v1/tools/create_client \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: onboard-acme-2026-10-24" \ -d '{"name":"Acme","domain":"acme.example"}' ``` | Answer | Meaning | | --- | --- | | The first answer | The call runs. Its answer — a result or a refusal — is kept for 24 hours under the key, for your agency only. | | The same status and body, with `Idempotent-Replayed: true` | The same key, tool and body within the day: the kept answer, and the call does not run again. | | `409` `idempotency_in_progress` | The first request under the key is still running. Retry when it finishes. | | `422` `idempotency_key_reused` | The key was used for a different tool or a different body. A new request needs a new key. | | A `5xx` | Not kept. The key is freed, so a retry under it runs the call again rather than replaying the failure — check whether the write landed first. | Choose a key that names what the call is for (`invoice-1042`, `onboard-acme-2026-10-24`): a key made fresh for each attempt protects nothing, because the retry carries a new one. Keys are per agency, so two agencies cannot collide. Reads need no key. The TypeScript SDK sends a key of its own with every write and keeps it across that call's retries, so a retry after a dropped connection is safe without you doing anything. Pass your own as `{ idempotencyKey }` in the call's options to keep it across processes too: ```ts await seo.call("send_report", { clientId, reportId }, { idempotencyKey: `report-${reportId}-2026-10` }); ``` A `403` does not tell you whether a client you may not reach exists: tenancy is checked before anything else, so a key pinned to one client learns nothing about the others from the error. ## Rate limits Limits are **per key**, not per agency: **600 requests a minute**, shared between REST and the MCP server. Every response to a recognised key — successes and failures alike — carries: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per window (600) | | `X-RateLimit-Remaining` | Requests left in the current window | | `X-RateLimit-Reset` | Seconds until the window resets | A request over the limit is answered `429` with `Retry-After` in seconds, the same number as `X-RateLimit-Reset`. A refused request does no work and buys nothing, so retrying it after the wait is always safe. A client that paces itself on `X-RateLimit-Remaining` never has to be refused to learn when to come back. The SDK and the command line wait for `Retry-After` on their own. ## Data that is not there Seolyticize returns `null` or leaves a field out where it has no measurement — a site that has not been crawled yet, an Analytics property that is not connected, a domain missing from the link graph — rather than a `0`. A zero in a response is a measured zero. --- # Costs and spending limits Most tools read data Seolyticize already holds and cost nothing. Some buy fresh data from a vendor — live search results, keyword volumes, AI assistant answers — and those cost money. Every one of them can be quoted before it runs and capped while it runs. ## Whose account pays Seolyticize has no shared vendor account. Paid calls are made on the **data-provider accounts your agency connects** under **Settings** (DataForSEO for search and keyword data, and the AI assistants you add keys for), so a charge lands on your own bill with that vendor. `get_vendor_connections` says which are connected and whether each last checked out — a call whose vendor was never connected is refused with a sentence saying so. ## Which calls cost money Discovery tells you per tool: - **`permission: "spend:incur"`** — the call can buy data. Only `manager` and `admin` keys may make it. Examples: `keyword_explorer`, `get_live_serps`, `serp_overview`, `run_ai_visibility_check`. - **`spends: true`** — the call can cost money. This includes the `spend:incur` tools and a few readers that buy on a cache miss (`get_domain_facts`, for one). Everything else is free. Data that was bought recently is answered from cache and costs nothing again, so the real charge is often lower than the quote. ## Quote first: `estimate_cost` `estimate_cost` is free. Give it a tool's name and the exact arguments you would call it with: ```bash curl -X POST https://seolyticize.com/api/v1/tools/estimate_cost \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"tool":"keyword_explorer","arguments":{"clientId":"","seed":"emergency dentist"}}' ``` ```json { "data": { "tool": "keyword_explorer", "estimatedMaxMicros": 75000, "purchases": [ { "what": "…", "maxMicros": 75000 } ], "monthlyMicros": null, "notes": ["Purchases answered from cache cost nothing, so the real charge is often lower."] } } ``` (The figures above are illustrative; the quote comes from the vendor's live pricing.) | Field | Meaning | | --- | --- | | `estimatedMaxMicros` | The most one call can cost, in micro-USD (1,000,000 = $1). A ceiling, never an average. | | `purchases` | Each purchase the call can make, with its own ceiling. | | `monthlyMicros` | For work that runs on a schedule, what a month costs at the current setting; otherwise `null`. | | `notes` | Anything that changes how to read the quote. | Arguments the target tool would refuse are refused here too, so a quote is always for a call that could actually run. `estimate_cost` also quotes a rank tracker's scheduled captures: pass `"tool": "rank_tracker"` with `clientId` and `trackerId`, and read `monthlyMicros`. ## Cap a call: `maxCostMicros` Every tool that can spend takes an optional **`maxCostMicros`**, in micro-USD, up to 1,000,000,000 ($1,000). A purchase that would take the call past it is refused **before anything is bought**, with a `422` saying so. :::stepper ::step{title="Quote"} Call `estimate_cost` with the arguments you intend to use. :: ::step{title="Agree"} Show `estimatedMaxMicros` to whoever is paying, or compare it to your own limit. :: ::step{title="Call with the quote as the cap"} Pass the quote as that call's `maxCostMicros`, so it cannot spend more than was agreed. :: ::: ```json { "clientId": "", "seed": "emergency dentist", "maxCostMicros": 75000 } ``` This is what Seolyticize's own assistant workflows do before every paid call. ## Cap a client: monthly budgets A client can carry a monthly budget, set when the client is added in the app, or with `create_client` / `update_client` (`monthlyBudgetUsd`; `null` removes it). Each purchase claims its share of the budget **before** the call is made, so two calls arriving together cannot both slip under the limit, and a purchase that would breach it is refused. If the vendor call then fails, the claim is released: an outage never eats a client's budget. ## See what was spent | Tool | Answers | | --- | --- | | `get_costs` | What the agency spent on data vendors per client over recent days (`sinceDays`, default 30), with request counts | | `get_client_costs` | What one client's data cost, by operation | Both need the `cost:view` permission (`manager` and `admin`). Amounts are US dollars. --- # MCP server The same tools are served over the Model Context Protocol, so an AI assistant can research keywords, read rankings, audit a site and draft a client report using your agency's data. ``` https://seolyticize.com/api/mcp ``` The server speaks **Streamable HTTP** and is stateless: every request carries its own credential, so there is no session to lose when a server restarts, and a revoked credential stops working on its very next message. An assistant is shown only the tools its credential may run, and each tool is marked read-only or not, so the host knows which ones to confirm with you first. There are two ways to connect. ## Sign in (OAuth) Clients that support MCP sign-in need no key. The first time a tool is used, the client opens a browser; you sign in to Seolyticize and choose: - **the agency** it may act for; - **what it may do** — a role, at most your own (`client_viewer`, `analyst`, `manager` or `admin`); - **which clients** — every client, or just one. An `admin` connection is always agency-wide. The connection then appears under **Settings → Connected apps**, where you can disconnect it at any time. :::tabs ::tab{title="Claude Code"} ```bash claude mcp add --transport http seolyticize https://seolyticize.com/api/mcp ``` Claude Code signs in through the browser the first time a Seolyticize tool runs. :: ::tab{title="Claude (web and desktop)"} **Settings → Connectors → Add custom connector**, with the URL above. Claude signs in the first time you use it. :: ::tab{title="ChatGPT"} **Settings → Apps & Connectors → Advanced settings**, turn on **Developer mode**, then **Create**: give it a name, paste the URL above, and choose **OAuth** for authentication. ChatGPT opens the sign-in page; afterwards the connector can be switched on in any chat. :: ::tab{title="Codex"} In `~/.codex/config.toml`: ```toml [mcp_servers.seolyticize] url = "https://seolyticize.com/api/mcp" ``` then `codex mcp login seolyticize` to sign in. :: ::tab{title="Cursor"} **Settings → MCP → Add new MCP server**, or in `~/.cursor/mcp.json`: ```json { "mcpServers": { "seolyticize": { "url": "https://seolyticize.com/api/mcp" } } } ``` Cursor asks you to sign in when the server first connects. :: ::tab{title="VS Code"} In `.vscode/mcp.json` (or **MCP: Add Server → HTTP**): ```json { "servers": { "seolyticize": { "type": "http", "url": "https://seolyticize.com/api/mcp" } } } ``` :: ::tab{title="OpenClaw"} ```bash openclaw mcp add seolyticize --url https://seolyticize.com/api/mcp --transport streamable-http ``` then set `auth: "oauth"` on the server under `mcp.servers.seolyticize` (or in **Settings → MCP**) so OpenClaw signs in, and check it with `openclaw mcp doctor seolyticize --probe`. :: ::: What happens underneath, for anyone writing their own client: | Step | Detail | | --- | --- | | Discovery | An unauthenticated request gets `401` with `WWW-Authenticate` naming the protected-resource metadata (RFC 9728), which names the authorization server (RFC 8414). | | Registration | Dynamic client registration at `/oauth/register` (RFC 7591). Public clients only: `token_endpoint_auth_method: none`. | | Authorization | Authorization code flow with **PKCE `S256` required**, at `/oauth/authorize`; tokens from `/oauth/token`. Scope: `mcp`. | | Tokens | Access tokens last **one hour**. Refresh tokens last **30 days** and rotate: each use returns a new pair and retires the old one. | If the person who approved a connection loses access to the agency, or their role falls below what they granted, the connection stops working at its next token exchange. ## API key Clients without MCP sign-in send an API key as a bearer token instead. Make the key under **Settings → API keys**; pin it to one client if the assistant only works for one customer. :::tabs ::tab{title="Claude Code"} ```bash claude mcp add --transport http seolyticize https://seolyticize.com/api/mcp \ --header "Authorization: Bearer $SEOLYTICIZE_API_KEY" ``` :: ::tab{title="Codex"} In `~/.codex/config.toml`, with the key exported as `SEOLYTICIZE_API_KEY` (it never goes in the file): ```toml [mcp_servers.seolyticize] url = "https://seolyticize.com/api/mcp" bearer_token_env_var = "SEOLYTICIZE_API_KEY" ``` :: ::tab{title="Hermes"} In `~/.hermes/config.yaml`, then `hermes mcp test seolyticize` and `/reload-mcp`: ```yaml mcp_servers: seolyticize: url: "https://seolyticize.com/api/mcp" headers: Authorization: "Bearer sk_seo_…" ``` Hermes names the tools `mcp_seolyticize_…`. :: ::tab{title="Gemini CLI"} In `~/.gemini/settings.json`: ```json { "mcpServers": { "seolyticize": { "httpUrl": "https://seolyticize.com/api/mcp", "headers": { "Authorization": "Bearer $SEOLYTICIZE_API_KEY" } } } } ``` :: ::tab{title="Claude Desktop (mcp-remote)"} In `claude_desktop_config.json`. The key stays in `env` rather than in `args`, where it would show in the process list: ```json { "mcpServers": { "seolyticize": { "command": "npx", "args": [ "-y", "mcp-remote", "https://seolyticize.com/api/mcp", "--header", "Authorization: Bearer ${SEOLYTICIZE_API_KEY}" ], "env": { "SEOLYTICIZE_API_KEY": "sk_seo_…" } } } } ``` :: ::: ## Limits and costs MCP calls share the key's rate limit with REST — 600 requests a minute per key — and follow the same rules on spending. A well-behaved assistant quotes a paid call with `estimate_cost` before making it and passes the quote as `maxCostMicros`; see [Costs and spending limits](./costs-and-spending-limits). ## Running it over stdio For a client that only launches a local command, the [command line](./command-line#mcp-over-stdio) relays the hosted server over stdio: install it with `npm install -g @seolyticize/cli`, then use `"command": "seolyticize", "args": ["mcp"]` with `SEOLYTICIZE_API_KEY` in its environment. ### Self-hosted For people running Seolyticize themselves: a checkout of the repository can serve the same tools as a local stdio server. It connects directly to the database and services named in the repository's `.env`, so it only works where those are reachable — everyone else should use the hosted endpoint above. ```bash SEOLYTICIZE_API_KEY=sk_seo_… pnpm --filter @seolyticize/web mcp ``` ```json { "mcpServers": { "seolyticize": { "command": "pnpm", "args": ["--filter", "@seolyticize/web", "mcp"], "env": { "SEOLYTICIZE_API_KEY": "sk_seo_..." } } } } ``` The key decides the whole session's reach: a key pinned to one client exposes exactly that client. ## Next Once connected, the [Agent playbooks](./start-here) walk an assistant through the whole job, from setting up the agency to the weekly pass over each client. --- # Agent skills Seolyticize's agent skills tell an assistant which tools to use for a job, in what order, and how to treat money and missing data, so "do keyword research for this client" is carried out the way an experienced analyst would, not improvised. Each is a folder with a `SKILL.md` in the open [Agent Skills](https://agentskills.io) format, so the same files work in every agent that reads them. They live in [Seolyticize/skills](https://github.com/Seolyticize/skills). ## Skills | Skill | For | | --- | --- | | `ai-visibility-audit` | Audit a client's visibility in AI assistants with Seolyticize. | | `alerts-and-monitoring` | Set up and run monitoring for a Seolyticize client - alert rules and where they deliver (email, Slack, webhook, Telegram), the default alerts, crawl schedules, deploy checks and deploy hooks, change detection between crawls, traffic anomalies, signed webhooks for events, and the weekly digest. | | `analytics-and-conversions` | Measure what search earns a Seolyticize client - read Search Console and GA4, install the cookieless first-party analytics snippet, define goals and funnels, collect server-side with issue_server_collect_key and visit tokens, record orders from any store with record_order / record_orders, and attribute revenue with get_seo_revenue. | | `client-onboarding` | Set up a new SEO client in Seolyticize end to end - create the client, write its brief from the website, connect Search Console, GA4 and first-party analytics, add the default alerts, schedule crawls, run the first crawl and start rank tracking, then confirm with get_next_steps. | | `competitor-analysis` | Work out who a Seolyticize client really competes with in search and where each rival wins. | | `content-brief-and-write` | Brief, draft, score and fix a page for a Seolyticize client. | | `content-opportunities` | Decide which content work to do first for a Seolyticize client. | | `deploy-check` | Check a release for SEO damage before and after it ships, with Seolyticize. | | `keyword-research` | Research and shortlist keywords for a Seolyticize client. | | `link-building` | Find, win and keep links for a Seolyticize client. | | `local-seo` | Audit and improve a business's local search with Seolyticize. | | `monthly-client-report` | Write and deliver the monthly SEO report a client actually reads - what changed, why, what it earned and what happens next - from Seolyticize evidence, following the agency's report template, saved as a written report, and sent or scheduled. | | `rank-tracking-review` | Review how a Seolyticize client's tracked keywords moved and why. | | `seo-audit-fix` | For a coding agent working inside a website's own repository. | | `seo-weekly-loop` | Run the weekly SEO check-in for one client with Seolyticize. | | `seolyticize` | The foundation for any work with Seolyticize, the SEO platform for agencies. | | `spend-and-costs` | Control and explain what Seolyticize spends on the agency's own DataForSEO and AI vendor accounts - quote calls before making them, set client budgets and alerts, read get_costs and get_client_costs, reconcile the vendor balance with get_vendor_reconciliation and explain any gap, and keep paid calls low. | | `technical-audit` | Run and triage a technical SEO audit of a client's site with Seolyticize. | Load `seolyticize` first if the agent has not used Seolyticize before: it covers the ways in, the conventions, and which skill to reach for. ## Connect the server The skills call Seolyticize's tools, so the agent needs the [MCP server](./mcp-server): `https://seolyticize.com/api/mcp`. The first call opens a browser to sign in and choose the agency, role and clients the agent may use. Without a browser, use an API key as `Authorization: Bearer $SEOLYTICIZE_API_KEY`, or the CLI's stdio bridge, `npx -y @seolyticize/cli mcp`. ## Install :::tabs ::tab{title="Any agent"} ```sh npx skills add Seolyticize/skills # choose the agents to install for npx skills add Seolyticize/skills -g --all # every skill, every agent, for your user ``` `-a ` picks one: `claude-code`, `codex`, `cursor`, `github-copilot`, `gemini-cli`, `antigravity`, `kiro-cli`, `cline`, `opencode`, `goose`, `windsurf` and about seventy more. :: ::tab{title="Claude Code"} ``` /plugin marketplace add Seolyticize/skills /plugin install seolyticize@seolyticize ``` The plugin brings the skills and the server together. :: ::tab{title="Claude apps and API"} Download a skill's zip from the [latest release](https://github.com/Seolyticize/skills/releases/latest) and upload it under **Customize → Skills** (older apps: **Settings → Capabilities**); custom skills need a paid plan with code execution on. Add `https://seolyticize.com/api/mcp` as a custom connector. On the API, upload the same zip through the Skills API and pass the server as an MCP connector. :: ::tab{title="Codex"} ```sh codex mcp add seolyticize --url https://seolyticize.com/api/mcp && codex mcp login seolyticize npx skills add Seolyticize/skills -a codex ``` :: ::tab{title="Cursor"} Add the server (Settings → MCP, or `~/.cursor/mcp.json` with `{"mcpServers":{"seolyticize":{"url":"https://seolyticize.com/api/mcp"}}}`), then `npx skills add Seolyticize/skills -a cursor`. Teams can import the repository as a marketplace (`.cursor-plugin/`) for the skills, a rule and the server together. :: ::tab{title="GitHub Copilot"} ```sh npx skills add Seolyticize/skills -a github-copilot ``` Skills go in `.github/skills/` (or `~/.copilot/skills/` with `-g`), for Copilot in VS Code, the Copilot CLI and the cloud agent. Add the server in `.vscode/mcp.json`: `{"servers":{"seolyticize":{"type":"http","url":"https://seolyticize.com/api/mcp"}}}`. :: ::tab{title="Gemini and Antigravity"} ```sh gemini extensions install https://github.com/Seolyticize/skills agy plugin import gemini # Antigravity CLI takes installed Gemini extensions across ``` The extension carries the skills, the server and a `GEMINI.md`. :: ::: Kiro, Cline, OpenCode, Goose, Windsurf and the rest: `npx skills add Seolyticize/skills -a `, then add `https://seolyticize.com/api/mcp` in the agent's MCP settings. An agent without skills support can read the repository's `AGENTS.md`. ## How every skill behaves - **It reads the client brief first** (`get_client_brief`), and records what it concluded in the brief's research log, so the next person or agent starts from it. - **It quotes before it spends.** Paid calls are quoted with `estimate_cost`, the total is shown to you, and the call carries the quote as `maxCostMicros`, so it cannot cost more than you agreed. Refining something already bought is free and preferred. - **It previews bulk writes** with `dryRun: true`, then asks. - **It never turns unknown into zero**, and says when a result is partial, provisional or a heuristic. - **It follows refusals.** Every refusal has a stable code and a `next` step; arguments a tool does not take are refused rather than ignored. --- # Agent quick start: a site in four calls For an AI agent that can change a site's code and deploy it. You connect the site completely — IndexNow, analytics, a crawl, and **Search Console with no person clicking in Google** — in four calls and one deploy. Base URL `https://seolyticize.com`. Every tool is `POST /api/v1/tools/{name}` with a JSON body and `Authorization: Bearer `, or the same tools over MCP at `https://seolyticize.com/api/mcp`. ## 1. Get a key for your site Ask the agency owner for an **invite code** (they make one with `create_invite_code`). Trade it, with your domain, for a key that reaches your site's client and nothing else. No key is needed for this call: ```bash curl -s -X POST https://seolyticize.com/api/v1/claim \ -H "Content-Type: application/json" \ -d '{"inviteCode": "inv_…", "domain": "example.com"}' ``` The answer has `apiKey` (shown once — store it as a secret), `clientId`, and the `site_admin` role: full control of that one client, none of the agency's settings, team, keys or billing. Alternatives: the owner can mint a key with `create_api_key {name, clientId, role: "site_admin"}`, or an MCP client can sign in with `claude mcp add --transport http seolyticize https://seolyticize.com/api/mcp` and have the owner approve it for one client in the browser — no key in any config file. ## 2. Onboard ```json onboard_site {"domain": "example.com", "framework": "nextjs"} ``` `framework` is one of `nextjs`, `astro`, `remix`, `sveltekit`, `nuxt`, `html`, `wordpress`, `other`. Calling it twice is safe: it finds the existing client instead of making another. It returns one **deploy manifest**: - `files` — each served at exactly `path` with exactly `content` (the IndexNow key and Google's verification file); - `head` — tags for the `` (only with `verification: "meta"`); - `analytics.install` — the analytics snippet placed for your framework, and `suggestedExclude` for signed-in areas (`data-exclude`); - `csp` — entries to add if the site sends a Content-Security-Policy; - `next` — what to do now. `verification` can be `file` (default), `meta`, or `dns` (one TXT record; covers every subdomain). ## 3. Deploy Commit and deploy the files, tags and snippet. Serve the files as-is: no redirect, no login, no HTML wrapper. ## 4. Verify ```json verify_site {"clientId": "client_…"} ``` Checks and finishes everything: the IndexNow file; Google ownership, then adds and connects the Search Console property (history included); the first analytics event; `robots.txt` and the sitemap; and starts the first crawl. It returns a checklist. Every item that is not `done` has `detail` (what is wrong) and `next` (the exact call, or what to deploy). Fix, redeploy, call it again. ## Then: the weekly loop ```json get_next_steps {"clientId": "client_…"} ``` The same checklist, read-only, plus `weekly`: the calls that find work ranked by payoff — `get_opportunities`, `striking_distance`, `get_low_ctr_queries`, `get_content_decay`, `get_cannibalization`, `list_crawl_issues` — each with its arguments filled in. After you publish changes, `submit_indexnow` with the changed URLs. ## Every deploy, automatically ```json create_deploy_hook {"clientId": "client_…"} ``` Returns a secret URL (once) and how to call it from GitHub Actions, Coolify, Netlify, Vercel or Cloudflare Pages. After each deploy the pipeline calls it — `curl -fsS -X POST ` — and the new and changed pages in the sitemap go to IndexNow. The first call records the baseline. A pipeline that knows what changed can send `{"urls": [...]}`. ## Errors Every failure is JSON: ```json { "error": "A sentence for you.", "code": "search_console_no_access", "next": "What to do.", "requestId": "req_…" } ``` Branch on `code`, never on the sentence. A key can carry a per-call spending ceiling; a call may ask for less with `maxCostMicros`, never more. Quote `requestId` when asking for help — it matches the server's log. Common codes: `invalid_input`, `unauthorized`, `forbidden`, `client_not_in_reach`, `refused`, `verification_not_found`, `search_console_no_access`, `google_not_ready` (wait a minute and retry), `rate_limited` (respect `Retry-After`), `internal_error`. ## Load only what you need `GET /api/v1/tools?group=onboarding` (or `weekly`, `technical`, `research`, `reporting`) lists just that job's tools with their schemas. Over MCP, connect to `https://seolyticize.com/api/mcp?group=onboarding`. Every tool stays callable by name either way. ## Analytics, safely The snippet sends no query string except `utm_*` tags, sends the referrer as an origin only, and never reports paths you exclude: `data-exclude="/app/*,/dashboard/*,/verify,/reset"` on the script tag. Or `data-include="/,/blog/*"` to report only those. More: [calling tools](./calling-tools) · [errors and limits](./errors-and-limits) · [the weekly playbook](./run-the-work) · full text for agents: `https://seolyticize.com/llms-full.txt` --- # Start here: running Seolyticize as an agent These playbooks are written for an AI agent doing an agency's SEO work through Seolyticize: onboarding clients, tracking and auditing them, reporting to them, and running the agency's own setup. Each step names the tool that does it. A tool is the same over REST (`POST /api/v1/tools/{name}`), over MCP (`/api/mcp`), in the SDK and in the CLI, so the playbooks work whichever way the agent connects. ## 1. Get access, once, from a person An agent cannot sign itself up. A person approves it once, in one of two ways: | Way | When | What the person does | | --- | --- | --- | | **Sign in (OAuth)** | the agent runs in Claude, ChatGPT, Cursor or another MCP client | adds `https://seolyticize.com/api/mcp` as a connector, signs in, and chooses the agency, the role and the clients | | **API key** | anything else: scripts, servers, other agent frameworks | creates a key under **Settings → API keys** and hands it over | After that the agent can mint narrower keys for its own integrations with `create_api_key` and retire them with `revoke_api_key`. A key can never be given more access than the credential that creates it. :::hint{style="info" title="Which role to ask for"} Creating clients, crawls, reports, report schedules, alert rules and portal links needs `client:manage`, which only **admin** holds. Team, provider keys, branding and billing need **admin** as well. A **manager** can track, research and spend but cannot onboard a client; an **analyst** can read and write data and send reports; a **client_viewer** only reads. Ask the person for the least the job needs, pinned to one client where the job is one client. ::: ## 2. Find out what this credential may do Call `whoami` first. It returns the agency, the role, every permission the role holds and the clients the credential reaches. Then list the tools this credential may call: `GET /api/v1/tools` over REST, or the MCP tool list. Each tool says whether it only reads (`readOnly`), whether it can cost money (`spends`) and its input as JSON Schema. Build against that list rather than a copy of these pages: it is generated from the code that runs the calls. ## 3. Keep spending in bounds Some tools buy data from a provider and cost money; they are marked `spends`. Before calling one: - `estimate_cost {tool, arguments}` quotes it without spending; - pass `maxCostMicros` on the call itself, and it is refused rather than run if it would cost more; - `get_costs` and `get_client_costs` show what has been spent; - a client can carry a monthly cap (`monthlyBudgetUsd` on `create_client` / `update_client`) that every call respects. Paid calls go to the agency's own provider accounts (see [Set up the agency](./set-up-the-agency)); without a key for the provider a tool needs, it is refused with a message saying which. ## 4. Work through the playbooks 1. [Set up the agency](./set-up-the-agency): provider keys, branding, team, plan. 2. [Onboard a client](./onboard-a-client): from a domain to tracking, an audit, a scheduled report and a portal link. 3. [Run the work](./run-the-work): what to check each week and what to do about it. 4. [Local, links and logs](./local-links-and-logs): map-pack grids and reviews, the link profile and outreach, crawler logs. 5. [What needs a person](./what-needs-a-person): the few acts an agent hands back, and how. 6. [What to fix, what to write](./fix-and-write): the weekly loop with data behind every step. 7. [Research any domain](./research-any-domain): any domain the way the big indexes read it, the AI answer index, any business on the map, and link prospects. ## Errors worth handling | You get | It means | | --- | --- | | `403` naming a permission | this credential's role does not include it — ask for a role that does, or a person | | a refusal saying a provider key is missing | the agency has not connected that provider yet | | a refusal over budget | `maxCostMicros` or the client's monthly cap would be exceeded; quote with `estimate_cost` first | | `429` with `Retry-After` | slow down and retry after that many seconds | The details are in [Errors and limits](./errors-and-limits). --- # Set up the agency Everything here is agency-wide and needs an **admin** credential (see [Start here](./start-here)). Do it once, before onboarding clients: most paid tools are refused until the provider they use is connected. :::stepper ::step{title="See what is connected"} `get_vendor_connections` lists the data providers the agency has connected and which are working. Nothing secret is returned — a provider, a hint of the key, when it was last checked and the last error. :: ::step{title="Connect the data providers"} `save_vendor_credential {provider, secret, login?}` stores a provider's key and checks it straight away; the response says whether the check passed. Providers: `dataforseo` (search volume, results pages, backlinks and most research), `serpapi`, `googleads` (keyword volume from the agency's own Ads account, with the `googleAds` object), `bingwebmaster`, `crux` and `pagespeed` (Core Web Vitals and lab runs), `serpproxy` (the agency's own proxy pool for results pages and AI answers), the AI assistants for AI visibility (`anthropic`, `chatgpt`, `gemini`, `perplexity`, `grok`, `meta-ai`, `deepseek`), `reportwebhook` and `telegram` (delivery). `remove_vendor_credential {provider}` disconnects one. :: ::step{title="Brand the reports"} `get_branding` reads, `set_branding {name, logoUrl, primaryColor}` sets what clients see on reports and portal links. Until it is set, reports go out unbranded. :: ::step{title="Add the team"} `list_team_members` shows who is in the agency, their roles and any client limits. `add_team_member {email, role, clientIds?}` adds a person who already has a Seolyticize account, as `admin`, `manager`, `analyst` or `client_viewer`, optionally limited to some clients. `remove_team_member {membershipId}` removes one; the last owner cannot be removed. For someone without an account yet, `invite_team_member {email, role, clientIds?}` emails them a link that lasts seven days and works once; the response also carries the link, for passing on by hand. `list_team_invites` shows the invites still open, `resend_team_invite {inviteId}` sends one again and `revoke_team_invite {inviteId}` withdraws it. :: ::step{title="Give integrations their own keys"} `create_api_key {name, clientId?, role?}` mints a key — shown once, in that response, never again. Pin it to a client where the integration only serves one. `list_api_keys` and `revoke_api_key {keyId}` manage them; `list_api_activity` shows which key called what. Apps that signed in over MCP are listed by `list_connected_apps` and removed with `disconnect_app {grantId}`. :: ::step{title="Group clients into portfolios"} `create_portfolio {name, members: [{clientId}]}` groups clients, or single sites of clients, under one name; `get_portfolio_metrics {portfolioId}` reads their clicks, rankings, site health and AI visibility together, with a row per member. A member without the data is left out of a total, never counted as zero. `list_portfolios`, `update_portfolio` and `delete_portfolio` manage them. :: ::step{title="Put an audit form on the agency's own site"} `create_lead_widget {name}` returns `embedCode` for a form where a visitor enters their URL and email and sees a short audit of the page on the spot. `list_leads` lists who filled it in; `update_lead_widget {widgetId, enabled: false}` turns a form off. :: ::step{title="Check the plan"} `get_billing` returns the plan, its limits (clients, tracked keywords) and status. Upgrading takes a person — see [What needs a person](./what-needs-a-person). :: ::: --- # Onboard a client The whole onboarding of one client, in order. It needs an **admin** credential for the steps marked _admin_; the rest work with less. Keep the ids each step returns — later steps need them. :::stepper ::step{title="Create the client (admin)"} `create_client {name, domain, keywords?, monthlyBudgetUsd?}` makes the client, a project, its site and a rank tracker in one call, and starts tracking any `keywords` given. It returns `clientId`, `projectId`, `siteId` and `trackerId`. Every tracked keyword is a paid lookup on every run, so set `monthlyBudgetUsd` if the client has a budget. :: ::step{title="Connect Google — ideally without a sign-in"} The quickest route an agent can finish itself: `get_google_service_account` returns the agency's own service account email. Ask your person to have the client's property owner add it — Search Console: **Settings → Users and permissions**, Restricted; GA4: **Admin → Property access management**, Viewer. Then call `connect_search_console_service_account {clientId}` (the site is tried as a Domain property, then `https://` and `https://www.`) and `connect_google_analytics_service_account {clientId, propertyId}`. Each checks with Google first; if access isn't granted yet, the refusal says exactly what the owner must do. :: ::step{title="Or ask a person to sign in"} Search Console and Google Analytics can only be connected by someone who can sign in to the client's Google account. `get_connection_link {clientId, service}` with `service` `search_console` (pass the property as `propertyUrl`) or `google_analytics` (pass `ga4PropertyId`, the numeric property id, not a `G-` measurement id) returns the link, what the person will approve, the steps, and what is connected now. Send the link to your person and carry on; check later with `get_client_integrations {clientId}`. Also available: `search_console_sitemaps`, `yandex_webmaster`, `google_business_profile`. For Yandex, `get_connection_link` also returns the steps for a person to make a token, and `connect_yandex_with_token {clientId, token}` connects what they hand you. :: ::step{title="Install analytics without Google"} For a site with no GA4, `set_up_web_analytics {clientId, hostnames?}` turns on Seolyticize's own cookieless analytics and returns the snippet to put in the site's ``. Read the results later with `get_web_analytics_summary {clientId}`. :: ::step{title="Set up rank tracking"} `create_client` made one tracker. Add keywords with `add_tracked_keywords {clientId, trackerId, terms}`, competitors with `add_competitors {clientId, trackerId, domains}`, and change device or frequency with `update_rank_tracker`. More trackers (another engine, location or device) come from `create_rank_tracker {clientId, name, engine?, device?, frequency?, locationCode?, languageCode?}`. To find keywords worth tracking: `keyword_explorer {clientId, seed}` (paid), `get_keyword_gap` / `get_content_gap {clientId, competitorDomains}`, and `find_serp_competitors {clientId, keywords}` (paid) to find who else ranks. :: ::step{title="Audit the site (admin)"} `start_crawl {clientId}` crawls the site with 144 checks. Poll `get_crawl_status {clientId, crawlId}` until it is `complete`, then read `get_audit_summary {clientId, crawlId}` and `list_crawl_issues {clientId, crawlId}` — each issue says how to fix it and how to check the fix. :: ::step{title="Track AI visibility"} `save_ai_visibility_setup {clientId, addBrands, addPrompts}` names the brand (and competitors) and the questions a buyer would ask; `suggest_ai_prompts {clientId}` (paid) drafts questions. `estimate_ai_visibility_cost {clientId}` quotes a run, `run_ai_visibility_check {clientId, maxCostMicros}` runs one, and `set_ai_visibility_schedule {clientId, cadence, engines}` repeats it. Which assistants are measured depends on the provider keys the agency has connected. :: ::step{title="Build and schedule the report (admin)"} `create_report {clientId, title, sections}` saves a report from `rank-summary`, `share-of-voice`, `keyword-movers`, `backlink-summary`, `traffic`, `core-web-vitals`, `site-health`, `audit-summary`. `schedule_report {clientId, reportId, recipients, cadence}` sends it monthly or weekly. A report goes only to its schedules' recipients: `send_report {clientId, reportId}` sends one now to all of them, so schedule before sending. :: ::step{title="Give the client a portal link (admin)"} `create_portal_link {clientId, label}` returns an address that shows the client their own dashboard under the agency's name, with no account. It is in that response once; send it to the client and keep it out of logs. `revoke_portal_link {clientId, linkId}` ends it. :: ::step{title="Set alerts"} `create_alert_rule {clientId, kind, threshold, channel, …}` — `kind` is `rank-drop`, `traffic-drop`, `new-critical-issues`, `backlinks-lost` or `site-unreachable`; `channel` is `email` (with `emailRecipients`), `slack` or `webhook` (with `webhookUrl`) or `telegram` (with `telegramChatId`). :: ::step{title="Write down what you learned"} `update_client_brief {clientId, overview, goal, positioning, audience, voice}` keeps the client's context where every teammate and agent reads it first (`get_client_brief`). `draft_client_brief {clientId}` (paid) drafts one from the site. :: ::: --- # Run the work A routine an agent can run for each client every week. Start with `list_clients`, then for each client `get_client {clientId}` for its trackers and settings. ## What's new The loop picked up five things this release. Each is free unless marked paid. - **Five new alert kinds.** `create_alert_rule {clientId, kind, threshold}` now also takes `competitor-new-pages` (pages a tracked competitor's sitemap gained), `page-changes` (own pages whose title, description, H1 or canonical changed, or that went noindex or 404, between the last two audits), `ai-mention-drop` (points the AI mention or citation rate fell), `competitor-new-keywords` (tracked keywords a competitor started ranking for) and `web-mentions` (new pages naming the brand). They stay silent until there is something to compare, and nothing is news twice: `list_recent_alerts {clientId, since}` shows what fired. - **Landing pages.** `get_landing_pages {clientId, trackerId}` lists the pages the tracked keywords land on, with top-3 and top-10 counts, and the keywords where two of the client's pages compete. - **AI Mode beside rank.** `get_tracker_ai_mode {clientId, trackerId, limit}` (paid, one AI Mode read per keyword) puts beside each position whether Google's AI Mode cites the client and whom it cites instead. Quote it with `estimate_cost` first. - **Share of local voice.** `get_local_visibility {clientId, locationId, term}` now returns a competitor table: every business the grid saw, with the share of points where it holds a top-3 slot. - **Redirect maps.** `plan_redirect_map {clientId, oldCrawlId}` (or `oldUrls`, or `oldSitemapUrl`) proposes a 301 for each old address of a migration, with a confidence and the evidence, as JSON, CSV, `.htaccess` or an nginx map. ## What moved | Question | Tool | | --- | --- | | How did rankings change? | `get_rank_summary {clientId, trackerId}`, `get_rank_history {clientId, trackerId, days}` | | Where do the keywords sit now? | `get_position_distribution {clientId, trackerId}` | | How does the client compare with competitors? | `get_share_of_voice {clientId, trackerId}` | | Which pages carry the rankings? | `get_landing_pages {clientId, trackerId}` | | Does AI Mode cite us where we rank? | `get_tracker_ai_mode {clientId, trackerId}` (paid) | | What did searchers actually do? | `get_search_console_performance {clientId, from, to}` (needs Search Console) | | And on the site? | `get_ga4_overview {clientId}` (GA4) or `get_web_analytics_summary {clientId}` | | What fired? | `list_recent_alerts {clientId, since}` | ## What to do about it | Opportunity | Tool | | --- | --- | | Keywords just off page one | `striking_distance {clientId}` | | Pages that rank but are not clicked | `get_low_ctr_queries {clientId}` | | Pages losing traffic | `get_content_decay {clientId}` | | Everything ranked by value | `get_opportunities {clientId}` | | Technical problems | `list_crawls {clientId}` → `list_crawl_issues {clientId, crawlId}`; a fresh crawl with `start_crawl` | | A page not indexed | `inspect_url {clientId, url}`, then `submit_sitemap` or `submit_indexnow {clientId, urls}` | | Speed | `page_vitals {url}` (real visits) and `page_lab {url}` (paid lab run) | | AI assistants not naming the client | `get_ai_visibility_results {clientId}`, `get_ai_visibility_trend {clientId}`, `get_agent_readiness {clientId}` | | Who the assistants cite instead, and in what tone | `get_ai_cited_sources {clientId}`, `get_ai_visibility_sentiment {clientId}` | | Speed recorded with the audit | `get_audit_performance {clientId}` (Lighthouse sample and field Core Web Vitals) | | A title that will be cut off | `simulate_serp_snippet {clientId, url}` | | Audit issues to hand out | `create_tasks_from_audit {clientId}`, then `list_tasks {clientId}` | ## What to tell the client - Scheduled reports send themselves (`list_report_schedules {clientId}`); `send_report` sends one now. - `create_report_link {clientId, reportId, expiresInDays}` gives a saved report its own branded page behind a signed, expiring link; `revoke_report_links {clientId, reportId}` turns every link to it off. Make one only when the person asks. - For a written update, `save_written_report {clientId, title, markdown}` stores it with the client and `set_report_sharing {clientId, reportId, shared: true}` gives it a share link. - Add what you learned to the brief with `update_client_brief`, so the next pass — yours or a person's — starts from it. ## Keep an eye on cost `get_client_costs {clientId}` each week. If a client is near its cap, slow the paid work (fewer tracked keywords, weekly instead of daily trackers with `update_rank_tracker`) rather than letting calls be refused mid-run. ## Refresh, research and housekeeping - `refresh_rankings {clientId, maxCostMicros}` (paid) captures rankings now; `refresh_domain_data` (paid) refreshes the domain overview; read either with `get_run_status`. - Research: `get_brand_mentions`, `get_keyword_trends`, `estimate_domain_traffic`, `get_subdomain_traffic` (all paid) and `get_domain_technologies` (free). - Keyword lists: `query_keyword_list {listId}` filters and sorts a list by text, tags, volume and difficulty (CSV with `format: "csv"`), `tag_list_keywords` tags keywords, `refresh_keyword_list_metrics` (paid) brings the numbers up to date, `rename_keyword_list`, `delete_keyword_list`, and `add_list_to_tracker` to track a whole list. - Rank tracking from a city: `search_locations {clientId, query, country}` finds the location code `create_rank_tracker` takes. - Indexing: `get_indexnow_key`, `issue_indexnow_key`, `verify_indexnow_key`, and `queue_url_inspections` for up to 500 URLs (results in `get_index_coverage`). - Backlinks: `build_disavow_file` returns a disavow file's text; nothing is submitted. --- # Local, links and logs ## Local search 1. `add_local_location {clientId, name, lat, lng}` (admin) adds a business location; `list_local_locations {clientId}` lists them. 2. `run_local_grid_scan {clientId, locationId, term, size, maxCostMicros}` (paid) checks the map pack street by street on a 3, 5, 7, 9 or 13 point grid. It runs in the background. 3. `get_local_visibility {clientId, locationId, term}` returns the latest grid, the review analysis and a competitor table: every business the grid saw, with its share of local voice (the share of points where it holds a top-3 slot), points seen, average position, rating and reviews. `run_local_grid_scan` takes `spacingKm` for the distance between points. 4. `sync_local_reviews {clientId, locationId, maxCostMicros}` (paid) refreshes reviews. With the client's Business Profile connected (`get_connection_link` with `google_business_profile`, done by a person) reviews come from their own listing. **Changing the listing.** With the Business Profile connected, `update_business_profile {clientId, changes, dryRun: true}` (client:manage) previews a change to the client's own listing — regular and special hours, description, phone, website, categories, yes/no attributes — as the exact patch, a before/after diff and Google's own validation; run it again without `dryRun` to save. `create_business_post {clientId, summary, callToAction?, dryRun: true}` does the same for a post. Both refuse with `missing_scope` when the connection was made without management access, and `next` says how to grant it. For a single point rather than a grid, `get_maps_results {clientId, keyword}` (paid) reads Maps or the Local Finder as searched from one spot. To look at any business on the map, not only the client's (`get_business_profile`, `search_local_businesses`, `get_local_categories`, `get_business_questions`, `get_business_reviews`), see [Research any domain](./research-any-domain#any-business-on-the-map). ## Backlinks - `get_backlink_audit {clientId, moneyTerms?}` — the client's links scored, top referring domains and anchors; `moneyTerms` flags commercial anchors. - `get_link_checks {clientId}` — whether linking pages still link (a page that redirected is `moved`, an app shell `unverifiable_js`: neither is a lost link); `recheck_links {clientId}` (admin) checks again, and `recheck_links {clientId, render: true}` reads the `unverifiable_js` pages in a real browser, up to 25 a run (poll `get_run_status` with job `links`). - `get_backlink_health {clientId, industry?}` and `get_toxic_link_risk {clientId, moneyTerms?}` — the profile's health and each site's risk, with disavow advice. Pass `fetchSpamScores: true` (paid, quote it with `estimate_cost`) to buy the vendor's spam scores inside the call; scores bought in the last 30 days are reused free. `get_backlink_spam_scores {clientId, domains, maxCostMicros}` (paid) buys them on their own. - For any domain, managed or not: `batch_domain_metrics {domains}` (authority from the Common Crawl link graph, free) and `get_link_intersect`. - `get_backlink_history {clientId, target}` (paid) — backlinks and referring domains month by month, for the client or a rival. ## Outreach `list_link_prospects {clientId}`, `add_link_prospects {clientId, prospects}` (admin) and `set_link_prospect_status {clientId, prospectId, status}` (admin) keep the outreach list with the client, so a person and an agent work from the same one. Where the prospects come from: - `get_competitor_broken_pages {clientId, domain}` — a rival's dead pages that sites still link to (broken-link building). On the client's own domain it lists links to reclaim with a redirect. - `get_outbound_domains {clientId, domain}` — the domains a site links out to: who a resource page or a rival already links to. - `get_forum_opportunities {clientId}` — Reddit, Quora and forum threads ranking for the client's keywords. - `search_web_content {clientId, keyword}` — pages that mention a phrase, with sentiment and domain rank: unlinked brand mentions, roundups, guest-post targets. ## Server logs `analyse_server_log {clientId, contents}` takes an access log as text (up to 64 MB per call) and returns what search and AI crawlers spent their time on: which pages they fetched, and how much of the budget went to errors and redirects instead of content. --- # What needs a person An agent can run nearly everything. A few acts stay with a person on purpose: they need someone's own login elsewhere, they spend the agency's money on a plan, or they cannot be undone. For each, a tool prepares the handoff so the person has one thing to click. | Act | Why a person | What the agent does | | --- | --- | --- | | Approve the agent in the first place | access is granted by someone accountable for the agency | ask them to add the MCP connector and sign in, or to create a key under **Settings → API keys** | | Grant access to Search Console or GA4 | only the property's owner can add a user | `get_google_service_account` and ask them to add that address; then connect it yourself with `connect_search_console_service_account` / `connect_google_analytics_service_account` | | Make a Yandex Webmaster token | it is minted in the site owner's Yandex account | `get_connection_link` with `yandex_webmaster` gives them the steps; connect what they hand you with `connect_yandex_with_token` | | Connect by sign-in instead (and Business Profile, which has no other route) | it needs a sign-in to the client's Google or Yandex account | `get_connection_link {clientId, service}` and send them the link; check with `get_client_integrations` | | Change the plan | it charges the agency | `get_billing_link {plan}` returns a checkout or billing-portal address for them to open | | Make someone an owner | owners can do anything, including close the agency | ask an owner to do it in **Settings → Team** | | Sign up a new teammate | an account is a person's own | `invite_team_member {email, role}` emails them a link to sign up and join; for someone already on Seolyticize, `add_team_member {email, role}` | | Close the agency or export all its data | irreversible, or the whole account at once | ask an owner to use **Settings → Ending this account** or **Data export** | When a tool is refused for a permission the credential lacks, the refusal names the permission. Tell your person which act needs it rather than asking for a broader credential than the job requires. --- # What to fix, what to write Two questions every week: **what should we fix or improve on the site**, and **what should we write next, shaped to rank**. Each step below is one call; the human approves. ## 1. What moved ```text get_search_changes {clientId, brand: "non_branded"} → new, lost, gainers, losers (28 days vs the 28 before) get_search_changes {clientId, dimension: "page"} → the same by page get_brand_split {clientId, interval: "week"} → is non-branded growing get_query_page_swaps {clientId} → queries now answered by a different page get_search_timeline {clientId} → traffic beside deploys, publishes and Google updates get_serp_volatility {clientId} → is it us, or is the whole results page moving ``` Set the brand terms once with `update_client_brief {clientId, brandTerms: [...]}`; until then the client's name and domain are used. `country: "esp"` segments any of these by market. ## 2. What to fix ```text compare_crawls {clientId} → new issues and fixed ones since the last crawl get_issue_templates {clientId} → each issue by URL template: 62 pages, one fix get_parameter_report {clientId} → ?sort= and ?filter= copies wasting the crawl → set_crawl_settings stripParams get_internal_link_graph {clientId} → orphans, pages deeper than three clicks, internal PageRank suggest_internal_links {clientId} → for pages in striking distance: the sentence to link from, and the anchor get_anchor_overlap {clientId} → one anchor pointing at two pages get_rendering_report {clientId} → content that exists only after JavaScript (turn on with set_crawl_settings render) get_broken_backlinks {clientId} → links already earned, to pages that 404, with where to redirect get_audit_performance {clientId} → Lighthouse on each large template, and field Core Web Vitals suggest_meta_tags {clientId} → titles and descriptions for every missing, duplicate or overlong tag in the crawl simulate_serp_snippet {clientId, url} → where Google will cut the title and description, in pixels plan_redirect_map {clientId, oldCrawlId} → before a migration: a 301 for each old address, as CSV, .htaccess or nginx ``` ## 3. What to write ```text get_topic_map {clientId, seed?} → clusters with no page (write), one page, or several (merge) cluster_keywords {clientId, keywords} → which keywords belong on one page get_demand_matrix {clientId, pattern, slots} → which template pages deserve to exist get_competitor_new_pages {clientId} → what competitors published, by section get_best_pages_by_links {clientId, domain: rival} → what earns links in the niche get_ai_overview_citations {clientId} → who the AI Overview cites for our keywords research_topic {clientId, seed} → subtopics, questions and the headlines ranking now (paid) get_personal_keyword_difficulty {clientId, keywords} → how hard each keyword is for this site forecast_keyword_traffic {clientId, keywords} → what the set is worth at positions 1, 3, 5 and 10 ``` ## 4. Write it to rank ```text build_content_brief {clientId, keyword, save: true} → intent, format, length, outline, must-cover terms, questions, links, competitor gaps draft_article {clientId, keyword} → a first draft from the brief (paid), or the writer drafts score_content {clientId, keyword, markdown} → 0–100 with the fix list; repeat until at par rewrite_section {clientId, keyword, section, draft} → a weak section rewritten to cover what the score found missing (paid) (a person reads it) (publish) submit_indexnow {clientId, urls} → or the deploy hook does it add_annotation {clientId, on, kind: "publish", title} → so next month's chart says why ``` ## 5. Measure it in sign-ups ```text get_conversions {clientId, channel: "search"} → conversions by landing page, with the queries that bring them list_conversion_goals {clientId} → what the site sends ``` Anything the tools above do not answer: `query_data` over the same stored data, and `export_data` for a notebook or BigQuery — see [Query and export](./query-and-export). ## What it costs Most of this is free: Search Console, the crawl, the link graph, analytics and the timeline are stored data. Results pages, keyword volumes and backlinks are paid lookups on the client's DataForSEO account — and served free when anyone on Seolyticize bought the same one recently. Every paid call returns `spend`: what it cost, what was reused, and what is left of the client's month. --- # Research any domain The tools on this page work on any domain, keyword or business, managed or not. Each still takes a `clientId`, because the paid lookups are billed to that client's DataForSEO account and every result is kept with the client. Most of them spend money: quote each call with `estimate_cost {tool, arguments}` and pass the quote as `maxCostMicros`. A lookup anyone on Seolyticize bought recently is served free, and the result's `spend` says which. ## A domain, the way the big indexes read it `target` is a domain (`rival.com`), a subfolder (`rival.com/blog/`) or a page URL, and defaults to the client's own domain. ```text get_domain_overview {clientId, target, historyMonths: 12} → organic and paid keywords, traffic and its value, positions, a monthly history get_ranked_keywords {clientId, target} → every keyword it ranks for, with position, URL, traffic, volume and intent get_ranked_keywords {clientId, target, resultType: "ai_overview_reference"} → the keywords whose AI Overview cites it get_top_pages {clientId, target} → its pages by estimated traffic get_backlink_history {clientId, target} → backlinks and referring domains month by month get_keyword_metrics {clientId, keywords: [...]} → volume, CPC, difficulty and intent for up to 700 known keywords get_serp_history {clientId, keyword} → who ranked for a keyword on past dates get_keyword_advertisers {clientId, keyword} → the ads shown for a keyword now, and who runs them ``` Over REST: ```bash curl -X POST https://seolyticize.com/api/v1/tools/get_ranked_keywords \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"clientId": "client_123", "target": "rival.com/blog/", "change": "new", "sort": "traffic", "limit": 50, "maxCostMicros": 50000}' ``` ## The AI answer index DataForSEO keeps a database of real AI answers. These tools read it, with no question tracking set up first. `platform` is `google` (AI Overviews) or `chat_gpt`. ```text get_ai_brand_mentions {clientId, brands: [{name, domains}]} → share of voice in AI answers for the client and up to nine rivals get_ai_prompt_research {clientId, keyword} → real questions people ask AI, with the sources and brands in each answer get_ai_search_volume {clientId, keywords: [...]} → how often a topic is asked of AI, with 12 months of history get_ai_top_cited_domains {clientId, topic} → the sites AI answers cite on a topic: the outreach and PR list get_ai_mode_answer {clientId, keyword} → Google AI Mode's answer and every source it cites get_llm_responses {clientId, prompt, platforms} → ask ChatGPT, Claude, Gemini and Perplexity live, with what each cited ``` To start tracking AI visibility from the client's own data, `suggest_ai_visibility_setup {clientId}` drafts the brand, the competitors and starter questions. Nothing is saved until you call it again with `apply: true`. Once runs exist, `get_ai_cited_sources {clientId}` lists who the assistants cite and who owns each source, `get_ai_visibility_sentiment {clientId}` reads how the answers speak of the brand, and `assess_ai_visibility_sentiment {clientId}` (paid, one Claude call per answer) judges the answers not yet judged. ## Any business on the map `business` is a name (best with the town), or `cid:…` / `place_id:…` for an exact listing. ```text get_business_profile {clientId, business} → categories, hours, rating spread, attributes, claimed, review topics search_local_businesses {clientId, lat, lng, radiusKm, categories} → every business within a radius get_local_categories {clientId, lat, lng} → the categories used nearby, and the attributes common among them get_maps_results {clientId, keyword} → Maps or Local Finder results from one point, and where the client sits get_business_questions {clientId, business} → the listing's Q&A, unanswered questions counted get_business_reviews {clientId, business} → queues the reviews; call again with the taskId to collect them free ``` For the map pack across a whole town, a grid scan is still the tool: see [Local, links and logs](./local-links-and-logs). ## Link prospects ```text get_competitor_broken_pages {clientId, domain} → a rival's dead pages that other sites still link to get_outbound_domains {clientId, domain} → the domains a site links out to get_forum_opportunities {clientId, keywords} → Reddit, Quora and forum threads ranking for the client's keywords search_web_content {clientId, keyword} → pages on the web that mention a phrase, with sentiment and domain rank ``` `get_competitor_broken_pages` with `source: "index"` reads our own crawl, which is free but covers only pages we have fetched. Keep the prospects you mean to contact with `add_link_prospects`, so a person and an agent work from the same list. ## Content from the research ```text research_topic {clientId, seed} → subtopics, questions and the headlines ranking now get_personal_keyword_difficulty {clientId, keywords} → how hard each keyword is for this site, not for an average one forecast_keyword_traffic {clientId, keywords: [{term}]} → clicks at positions 1, 3, 5 and 10, month by month (free) draft_article {clientId, keyword} → a Markdown draft grounded in the content brief (a paid Claude call) rewrite_section {clientId, keyword, section, draft} → a section rewritten or extended to cover what the score finds missing suggest_meta_tags {clientId} → titles and descriptions for a whole crawl's missing, duplicate or overlong tags ``` Score every draft with `score_content` before it is published, and have a person read it: see [What to fix, what to write](./fix-and-write). --- # TypeScript SDK `@seolyticize/sdk` is a typed TypeScript client for the API. Every tool is a method, and its input types are generated from the registry that validates the calls, so the SDK cannot describe a tool differently from how the server runs it. It has no runtime dependencies. It needs `fetch`, which Node 18+, Deno, Bun and browsers all have. :::hint{style="info" title="Installing"} ```bash npm install @seolyticize/sdk ``` Source: [github.com/Seolyticize/sdk-typescript](https://github.com/Seolyticize/sdk-typescript). Python and Go: [Python SDK](./python-sdk), [Go SDK](./go-sdk). ::: ## Quick start ```ts import { createClient } from "@seolyticize/sdk"; const seo = createClient({ apiKey: process.env.SEOLYTICIZE_API_KEY! }); // Typed: the input is checked against the tool's schema at compile time. const { clients } = await seo.tools.list_clients<{ clients: Array<{ id: string; name: string }>; }>(); const summary = await seo.tools.get_rank_summary({ clientId: clients[0]!.id, trackerId: "", }); ``` Results are typed `unknown` unless you pass a type argument. The API publishes input schemas, not output schemas, and a guessed output type would be a promise nothing keeps. ### Tools newer than your SDK Tools are added to the platform continuously. Call one your version has never heard of by name: ```ts const result = await seo.call("some_new_tool", { clientId: "" }); ``` ### What may this key do? ```ts const { caller, tools } = await seo.listTools(); // caller.role, caller.clientIds ("all", or the one client), and every tool // the key may call with its JSON Schema. ``` ## Options ```ts createClient({ apiKey: "sk_seo_…", baseUrl: "https://seolyticize.com", // the default maxRetries: 2, // retries after the first attempt timeoutMs: 30_000, // per attempt maxRetryAfterSeconds: 60, // longest Retry-After the client will wait through fetch: customFetch, // optional }); ``` Pass `{ signal }` as the last argument of any call to cancel it. ## Errors Every failure is a `SeolyticizeError` with `status`, `tool` and the server's own sentence as `message`. Branch on the subclass: | Class | Status | Meaning | | --- | --- | --- | | `ValidationError` | 400 | Input did not match the tool's schema; the message names fields | | `AuthenticationError` | 401 | Missing, unknown or revoked key | | `PermissionError` | 403 | The key's role or client scope does not allow this | | `NotFoundError` | 404 | No tool by that name | | `RefusedError` | 422 | Well-formed, but declined — duplicate name, plan limit, spending limit | | `RateLimitError` | 429 | Window used up; `retryAfterSeconds` says how long to wait | | `ServerError` | 5xx | Could not be completed; the server never says why | | `NetworkError` | — | No response at all (DNS, TLS, timeout) | ```ts import { RefusedError, RateLimitError } from "@seolyticize/sdk"; try { await seo.call("create_client", { name: "Acme", domain: "acme.com" }); } catch (error) { if (error instanceof RefusedError) console.log("Declined:", error.message); else if (error instanceof RateLimitError) console.log("Wait", error.retryAfterSeconds); else throw error; } ``` ## Retries - **429** is always retried, waiting as long as `Retry-After` asks (up to `maxRetryAfterSeconds`; a longer wait throws `RateLimitError` straight away). Nothing ran, so repeating is safe. - **5xx and network failures** are retried with exponential backoff and jitter **only for read-only tools**. A write that failed with a `500` may already have happened — repeating `create_client` could create two clients — so writes surface the error instead. A tool the SDK has never heard of is treated as a write. - 400, 401, 403, 404 and 422 are never retried; they would fail the same way. `client.lastRateLimit` holds the `X-RateLimit-*` headers from the latest response. --- # Command line The `seolyticize` command calls any tool from a terminal or a script. It reads the tool list from discovery when it runs, so a tool added to the platform today is a command today, without a new release. It is built on the [TypeScript SDK](./typescript-sdk), and waits out rate limits the same way. :::hint{style="info" title="Installing"} ```bash npm install -g @seolyticize/cli seolyticize --help ``` Source: [github.com/Seolyticize/cli](https://github.com/Seolyticize/cli). ::: ## Log in ```bash # Piped, so the key never lands in shell history: printf '%s' "$KEY" | seolyticize login # Or interactively (the key is not echoed): seolyticize login ``` The key is checked against the API before it is stored, in `~/.config/seolyticize/config.json` (or `$XDG_CONFIG_HOME/seolyticize/config.json`), with permissions `0600` on the file and `0700` on its directory. `SEOLYTICIZE_API_KEY` and `SEOLYTICIZE_BASE_URL` override the stored values, which is how CI should provide them. ```bash seolyticize whoami # role, reach (agency-wide or one client), tool count seolyticize logout ``` ## Call tools ```bash seolyticize tools # what this key may call seolyticize describe get_top_searches # a tool's options seolyticize call get_top_searches --market=uk --since=2026-09-01 --limit=20 seolyticize get_top_searches --market=uk --since=2026-09-01 # same, without "call" ``` Input can also come as JSON, inline, from a file, or from stdin; options apply on top: ```bash seolyticize call get_keyword_gap --input '{"clientId":"","competitorDomains":["a.com"]}' seolyticize call get_keyword_gap --file gap.json cat gap.json | seolyticize call get_keyword_gap --file - ``` Options are typed from the tool's schema: numbers and booleans are converted, arrays take repeated options or commas (`--domains=a.com,b.com`), objects take JSON, `null` is null where the schema allows it, and `--client-id` is accepted for `clientId`. ### Shortcuts ```bash seolyticize clients # list_clients seolyticize rank --client # get_rank_summary seolyticize alerts --client [--since 2026-10-01T00:00:00Z] # last 7 days by default ``` ## Output Human-readable tables by default. Scripts should pass `--json`, which prints the tool's result exactly as the API returned it under `data`: ```bash seolyticize clients --json | jq -r '.clients[].id' ``` Errors go to stderr; results go to stdout. ## Exit codes Stable, so scripts can branch on them: | Code | Meaning | | --- | --- | | 0 | success | | 1 | unexpected error | | 2 | usage error (bad command, option or JSON) | | 3 | authentication failed (missing, unknown, revoked) | | 4 | not permitted (role or client scope) | | 5 | not found (no such tool for this key) | | 6 | invalid input (did not match the tool's schema) | | 7 | refused (a well-formed request the product declined) | | 8 | rate limited (after the SDK's retries) | | 9 | server error or network failure | ## MCP over stdio Some MCP clients only launch a local command. `seolyticize mcp` relays the hosted MCP server over stdin and stdout with your key, so those clients get the same tools as everyone else: ```json { "mcpServers": { "seolyticize": { "command": "seolyticize", "args": ["mcp", "--group", "onboarding"], "env": { "SEOLYTICIZE_API_KEY": "sk_seo_…" } } } } ``` `--group` (`onboarding`, `weekly`, `technical`, `research`, `reporting`) lists only that job's tools. Clients that speak HTTP should connect to `https://seolyticize.com/api/mcp` directly. --- # Web analytics without the script The analytics snippet (`` | — | Each kit does the same four things: 1. **The analytics tag**, with `data-exclude` for paths that should never be reported (accounts, checkout, verification links). 2. **The IndexNow key file** at `/KEY.txt`: the key and nothing else, as plain text. 3. **Deploy notification**: one POST to the deploy hook after each deploy; Seolyticize reads the sitemaps and sends what changed to IndexNow. 4. **Server-side events** through the [collect API](./collect-api), for conversions that happen on the server. The WordPress plugin also sends a post to IndexNow the moment it is published or updated, and shows Seolyticize's findings for a page beside it in the editor. --- # Query and export The tools answer the common questions. For the rest, `query_data` asks the stored data directly and `export_data` hands it over as a file. Both read only the client's own data, read-only. ## Datasets `describe_datasets` lists them with their columns: | Dataset | What it holds | | --- | --- | | `gsc` | Search Console, every day since the site was connected: date, query, page, country, device, clicks, impressions, plus `ctr` and `position` | | `web_events` | Cookieless analytics: pageviews and custom events, with path, source, referrer, campaign, country, device, value | | `crawl_pages` | A crawl's pages: status, title, H1, words, depth, indexable (`crawlId`, default latest) | | `crawl_issues` | A crawl's issues by page | | `crawl_links` | A crawl's internal links with anchors | | `rank_captures` | Rank tracking: keyword, date, device, position, URL | | `backlinks` | Links to the client's domain from our own link index | ## A query ```json { "clientId": "client_…", "dataset": "gsc", "dimensions": ["query"], "metrics": ["sum(clicks)", "sum(impressions)", "position"], "filters": [ { "column": "query", "op": "contains", "value": "sms" }, { "column": "country", "op": "eq", "value": "esp" }, { "column": "device", "op": "eq", "value": "MOBILE" } ], "from": "2026-07-01", "orderBy": [{ "column": "sum(clicks)", "desc": true }], "limit": 100 } ``` Metrics are `count`, `sum(col)`, `avg(col)`, `min(col)`, `max(col)`, `uniq(col)`, and the dataset's own (`ctr`, `position` for Search Console; `visitors`, `visits`, `pageviews` for analytics). Filters: `eq`, `neq`, `contains`, `not_contains`, `starts_with`, `gt`, `gte`, `lt`, `lte`, `in`, `regex`. With no dimensions or metrics, `columns` lists rows. Search Console data is kept at several grains; the query reads the one that carries every column it names, so `query` with `page` and `country` reads the full grain, which only reaches back as far as a connection's history has been filled. ## An export The same query plus `format` (`csv`, `jsonl` or `parquet`) and up to a million rows. The answer is a download link valid for 24 hours: ```json { "dataset": "gsc", "format": "parquet", "bytes": 8340211, "url": "https://seolyticize.com/api/v1/exports/…", "expires": "…" } ``` Load a Parquet export into BigQuery with `bq load --source_format=PARQUET dataset.table export.parquet`, or into pandas with `pd.read_parquet`. --- # Server-side collection and visit tokens The analytics snippet reports from the visitor's browser. A server collector reports from your backend instead: for AI agents and crawlers that never run a script, for frameworks that render on the server, and for conversions that happen after the visitor has left — a payment confirmed an hour later, a trial that converts next week. Three pieces, each with a curl example below: 1. **The collect request from a server**, with the visitor's own address (`ip`) and user agent (`ua`), authenticated with the site's server key. 2. **The visit token** the `202` answer carries, kept on your server and sent with a later event. 3. **`record_order`** for orders from anywhere, credited to the visit by its token, with an `Idempotency-Key` so a retried push never counts twice. The request fields every event shares (`k`, `n`, `u`, `r`, `p`) are on [Web analytics without the script](./collect-api), with the full table of answers. ## The request from a server The visitor hash uses the address and user agent the request comes from. Sent from your own server, every event would share the server's address and count as one visitor. So a server collector authenticates with the site's **server key** — from `issue_server_collect_key`, shown once, kept in an environment variable and never in a page — and passes each visitor's own address and user agent in the body: | Field | Where | | | --- | --- | --- | | `Authorization` | header | `Bearer `. A wrong key is refused with `401` rather than counted as a browser. | | `ip` | body | The visitor's own address, as your proxy reported it: `CF-Connecting-IP`, `X-Real-IP`, or the client end of `X-Forwarded-For`. Hashed under a daily salt, never stored. Up to 64 characters. | | `ua` | body | The visitor's `User-Agent` header: the visitor hash, the bot check, and the browser, OS and device. Never stored whole. Up to 1,024 characters. | | `h` | body | The visitor's `Sec-CH-UA` client hints, if sent. Up to 512 characters. | | `sa` | body | The `Signature-Agent` header signing agents such as ChatGPT send, so an assistant is counted as one. Up to 256 characters. | | `c` | body | The visitor's country, ISO 3166-1 alpha-2, as your proxy said it (Cloudflare's `CF-IPCountry`). | | `visit` | body | A visit token, on an event that happens after the visit (below). `u` may then be left out. | These fields are read only from a request carrying the server key. From anyone else they are ignored, so a page cannot claim to be any visitor it likes. A server request needs no `Origin`; the page's hostname in `u` must still be one of the setup's. ```bash curl -X POST https://seolyticize.com/api/collect \ -H "Authorization: Bearer $SEOLYTICIZE_SERVER_KEY" \ -H "Content-Type: application/json" \ -d '{"k":"","n":"pageview","u":"https://example.com/pricing","r":"https://www.google.com/","ip":"203.0.113.7","ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36","c":"GB"}' ``` The answer is `202` with the visit's token. Keep it on your server if a conversion may follow later (below); otherwise ignore it: ```http HTTP/1.1 202 Accepted Content-Type: application/json {"visit":"v1.AAAAAZJ3kQ8xAKwF.8yq6Vv0m3QnXw1kJ2pE7bA"} ``` Only a person's visit gets a token. An AI agent, a bot or an excluded path is answered `202` with no body: there is no visit to credit a conversion to. The packaged collectors fill these in from the request's own headers, drop bots before sending, and never hold up the page: :::tabs ::tab{title="Node / Next.js"} ```ts import { addressFrom, createCollector } from "@seolyticize/next/server"; const collector = createCollector({ siteKey: "", serverKey: process.env.SEOLYTICIZE_SERVER_KEY!, only: "agents", // with the browser script installed too; "all" without it }); await collector.trackRequest(request); // reads ip, ua, h, sa and c from the headers // or name them: collector.track({ url, userAgent, ip: addressFrom(request.headers), country }) ``` :: ::tab{title="Python"} ```python import os from seolyticize.collect import Collector, WSGICollector collector = Collector("", os.environ["SEOLYTICIZE_SERVER_KEY"], only="agents") app.wsgi_app = WSGICollector(app.wsgi_app, collector) # Flask; ASGICollector, django_middleware too # or: collector.track(url, user_agent=ua, ip=ip, country=country) ``` :: ::tab{title="PHP"} ```php $collector = new \Seolyticize\Collector('', getenv('SEOLYTICIZE_SERVER_KEY'), ['only' => 'agents']); register_shutdown_function(fn () => $collector->trackCurrentRequest()); // reads $_SERVER // or: $collector->track($url, ['userAgent' => $ua, 'ip' => $ip, 'country' => $country]); ``` :: ::: With the browser script also installed, report only AI agents (`only: "agents"`), or every person is counted twice. Requests from bots are accepted and quietly dropped; one visitor sending too fast is answered `429`. ## Conversions after the visit The visitor id resets daily and nothing is stored on the device, so a conversion that happens later — a crypto top-up confirmed an hour after checkout, a trial that converts next week — cannot be tied to its visit by itself. A visit token does it: 1. While the visitor is on the site, the server collector reports an event for them (a sign-up, a checkout started) with their `ip` and `ua`. The `202` answer carries `{"visit": "v1.…"}`. 2. The site keeps the token **on its own server**, with the account or the order. It is never sent to a browser and goes in no cookie. 3. Later, the server sends the event with the token, and nothing about the visitor: ```bash curl -X POST https://seolyticize.com/api/collect \ -H "Authorization: Bearer $SEOLYTICIZE_SERVER_KEY" \ -H "Content-Type: application/json" \ -d '{"k":"","n":"Top-up","p":{"value":25},"visit":"v1.…"}' ``` The event is credited to the visit's landing page, source, campaign and channel, so goals (`get_goal_report`), funnels and `get_conversions` count it there; a `ref` or `order_id` property on it links a Stripe, Shopify or Paddle order to the visit for `get_seo_revenue`, and `record_order` takes the token as `visit` for orders from anywhere else. A credited event counts as a conversion in the period it happened and never as a visit. The token holds only the visit's random id and the minute it was issued, signed and bound to the site, so it cannot be guessed, edited to name another visit, or used on another site. It lasts 35 days. In the collectors: `trackForVisit()` / `trackLater()` (Node), `track_for_visit()` / `track_later()` (Python), `trackForVisit()` / `trackLater()` (PHP). ## Orders from anywhere A business on neither Stripe, Shopify nor Paddle pushes its orders with an API key (one pinned to the client is enough): `record_order` for one, `record_orders` for up to 500 per call. Each carries the visit token, when the site kept one, and is credited to that visit in `get_seo_revenue`. One order, credited to the visit whose token the site kept: ```bash curl -X POST https://seolyticize.com/api/v1/tools/record_order \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-inv_1042" \ -d '{"clientId":"","orderId":"inv_1042","amountMinor":4999,"currency":"USD","at":"2026-10-24T09:12:00Z","visit":"v1.AAAAAZJ3kQ8xAKwF.8yq6Vv0m3QnXw1kJ2pE7bA"}' ``` ```json { "data": { "orderId": "inv_1042", "outcome": "recorded", "attributedBy": "analytics", "channel": "organic", "landingPath": "/pricing", "note": null, "code": null } } ``` `attributedBy: "analytics"` means the token matched its visit, and `channel` and `landingPath` say where it is credited; `"none"` with a `note` means the token had expired or its visit was not found: the order still counts in revenue, unattributed. A token issued for another site is refused (`invalid_visit`) rather than guessed at. A batch, up to 500: ```bash curl -X POST https://seolyticize.com/api/v1/tools/record_orders \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: orders-2026-10-24T10" \ -d '{"clientId":"","orders":[ {"orderId":"inv_1042","amountMinor":4999,"currency":"USD","at":"2026-10-24T09:12:00Z","visit":"v1.…"}, {"orderId":"ref_77","amountMinor":1000,"currency":"USD","at":"2026-10-24T09:40:00Z","refundOf":"inv_0998"}]}' ``` `amountMinor` is in the currency's minor unit (4999 is 49.99 USD; 500 is 500 JPY). Recording an `orderId` again replaces the order, and an identical replay is answered `unchanged`, so a push can simply be repeated. A refund is an order of its own with `refundOf` naming the order it refunds: it nets off revenue in the period it happened and is credited where its order was. Each order is answered on its own — `recorded`, `updated`, `unchanged` or `refused` with a reason — and one refused order does not stop the rest. ## Retrying safely: Idempotency-Key Every write and every paid tool call takes an `Idempotency-Key` header (up to 200 characters). A retry under the same key within 24 hours — after a timeout or a dropped connection — gets the first answer back, marked `Idempotent-Replayed: true`, and does not run again: ```bash # The same request twice: the second is answered from the first. curl -i -X POST https://seolyticize.com/api/v1/tools/record_order \ -H "Authorization: Bearer $SEOLYTICIZE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-inv_1042" \ -d '{"clientId":"","orderId":"inv_1042","amountMinor":4999,"currency":"USD","at":"2026-10-24T09:12:00Z"}' # HTTP/1.1 200 OK # Idempotent-Replayed: true ``` | Answer | Meaning | | --- | --- | | `409` `idempotency_in_progress` | The first request under the key is still running; retry when it finishes. | | `422` `idempotency_key_reused` | The key was used for a different tool or body; a new request needs a new key. | | A `5xx` | Not kept: the key is freed, so a retry runs the call again. | Name the key after what it is for (`order-inv_1042`), not the attempt: a fresh key per attempt protects nothing. `/api/collect` itself needs no key — a repeated event is a repeated event, and the visitor rate limit answers `429` to a flood. The details are on [Errors and limits](./errors-and-limits#retrying-writes-idempotency-key). --- # Seolyticize API Seolyticize API - [Endpoints](ref-endpoints) - [Agency](ref-agency) - [Client](ref-client) --- # Endpoints Endpoints --- # List the tools this key may call ## List the tools this key may call **`GET https://seolyticize.com/api/v1/tools`** — List the tools this key may call Every tool the presented key's role permits, with its input as JSON Schema. Generated from the same registry that runs the calls, so it cannot describe a tool differently from how it behaves. ### Responses **200** — The catalogue as this key sees it. #### Fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `tools` | ToolDescriptor[] | yes | | | `tools.name` | string | yes | | | `tools.title` | string | yes | | | `tools.description` | string | yes | | | `tools.permission` | string | yes | | | `tools.clientScoped` | boolean | yes | | | `tools.readOnly` | boolean | yes | | | `tools.spends` | boolean | yes | | | `tools.path` | string | yes | | | `tools.inputSchema` | object | yes | | ```json { "tools": [ { "name": "string", "title": "string", "description": "string", "permission": "string", "clientScoped": true, "readOnly": true, "spends": true, "path": "string", "inputSchema": {} } ] } ``` **401** — No key, or a key that is unknown or revoked. #### Fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `error` | string | yes | A sentence written for the caller. Internal failures are reduced to 'The request could not be completed.' and never carry detail. | ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X GET "https://seolyticize.com/api/v1/tools" ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools', { method: 'GET', }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "GET", "https://seolyticize.com/api/v1/tools", ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://seolyticize.com/api/v1/tools", nil) res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Agency Agency --- # List clients ## List clients **`POST https://seolyticize.com/api/v1/tools/list_clients`** — List clients Lists every client the caller can access. Use this first when a request names a client by name rather than id. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. #### Fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `data` | | yes | | ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_clients" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_clients', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_clients", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_clients", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get panel reach ## Get panel reach **`POST https://seolyticize.com/api/v1/tools/get_panel_reach`** — Get panel reach How many panel members visited given hosts over a period, and how often. Answers 'what did real people actually do', as far as the panel saw. Returns the panelist count alongside the visits so a thin panel is visibly thin rather than quietly wrong. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `hosts` | string[] | yes | | | `from` | string | yes | | | `to` | string | yes | | ```json { "hosts": [ "string" ], "from": "string", "to": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_panel_reach" \ -H "Content-Type: application/json" \ -d '{"hosts":["string"],"from":"string","to":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_panel_reach', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"hosts":["string"],"from":"string","to":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_panel_reach", headers={ "Content-Type": "application/json", }, json={"hosts": ["string"], "from": "string", "to": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_panel_reach", strings.NewReader(`{"hosts":["string"],"from":"string","to":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Look up domains in bulk ## Look up domains in bulk **`POST https://seolyticize.com/api/v1/tools/batch_domain_metrics`** — Look up domains in bulk Authority, keywords, traffic and referring domains for any list of domains, managed or not. Answers 'how strong is this site' for competitors and prospects. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `domains` | string[] | yes | | ```json { "domains": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/batch_domain_metrics" \ -H "Content-Type: application/json" \ -d '{"domains":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/batch_domain_metrics', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"domains":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/batch_domain_metrics", headers={ "Content-Type": "application/json", }, json={"domains": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/batch_domain_metrics", strings.NewReader(`{"domains":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # How a page performs for real visitors ## How a page performs for real visitors **`POST https://seolyticize.com/api/v1/tools/page_vitals`** — How a page performs for real visitors Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift for one address, from the Chrome UX Report. Answers 'is this page fast enough for real visitors' with field data over a trailing 28 days, which is what Google ranks on rather than a lab score. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | | ```json { "url": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/page_vitals" \ -H "Content-Type: application/json" \ -d '{"url":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/page_vitals', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"url":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/page_vitals", headers={ "Content-Type": "application/json", }, json={"url": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/page_vitals", strings.NewReader(`{"url":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What the server actually returned ## What the server actually returned **`POST https://seolyticize.com/api/v1/tools/page_headers`** — What the server actually returned Status code, redirect chain, content type and the headers that decide indexing for one address. Answers 'why is this page not in the index' when the markup looks fine — an X-Robots-Tag noindex is invisible to anything reading the page. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "url": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/page_headers" \ -H "Content-Type: application/json" \ -d '{"url":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/page_headers', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"url":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/page_headers", headers={ "Content-Type": "application/json", }, json={"url": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/page_headers", strings.NewReader(`{"url":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What the panel saw people search for ## What the panel saw people search for **`POST https://seolyticize.com/api/v1/tools/get_search_demand`** — What the panel saw people search for How many people in the panel searched a term, and how often, in one market. Answers 'do real people actually search this' — a reading from people rather than a vendor's model. Returns nothing at all when too few people searched it, because a term seen by two panelists is those two people rather than a measurement. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | yes | | | `market` | string | yes | | | `since` | string | yes | | ```json { "query": "string", "market": "string", "since": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_search_demand" \ -H "Content-Type: application/json" \ -d '{"query":"string","market":"string","since":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_search_demand', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"query":"string","market":"string","since":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_search_demand", headers={ "Content-Type": "application/json", }, json={"query": "string", "market": "string", "since": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_search_demand", strings.NewReader(`{"query":"string","market":"string","since":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # How clicks fall away down a results page ## How clicks fall away down a results page **`POST https://seolyticize.com/api/v1/tools/get_click_curve`** — How clicks fall away down a results page The share of clicks each position took, as the panel observed it — for one query, or across a whole market. Answers 'what is a position actually worth here' with a measurement rather than a published average. Positions too few people clicked are withheld, so the shares deliberately do not add up to one: the rest of the page is unmeasured, not empty. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `engine` | string | no | | | `market` | string | no | | | `query` | string | no | | ```json { "engine": "google", "market": null, "query": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_click_curve" \ -H "Content-Type: application/json" \ -d '{"engine":"google","market":null,"query":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_click_curve', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"engine":"google","market":null,"query":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_click_curve", headers={ "Content-Type": "application/json", }, json={"engine": "google", "market": None, "query": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_click_curve", strings.NewReader(`{"engine":"google","market":null,"query":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # The terms the panel searched most ## The terms the panel searched most **`POST https://seolyticize.com/api/v1/tools/get_top_searches`** — The terms the panel searched most What a market actually searched for over a period, most-searched first, counted by distinct people rather than by events. Answers 'what is being looked for right now' for keyword discovery, without a vendor's list in between. Terms below the reporting floor never appear. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `market` | string | yes | | | `since` | string | yes | | | `limit` | integer | no | | ```json { "market": "string", "since": "string", "limit": 100 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_top_searches" \ -H "Content-Type: application/json" \ -d '{"market":"string","since":"string","limit":100}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_top_searches', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"market":"string","since":"string","limit":100}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_top_searches", headers={ "Content-Type": "application/json", }, json={"market": "string", "since": "string", "limit": 100}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_top_searches", strings.NewReader(`{"market":"string","since":"string","limit":100}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # How a page scores in one synthetic run ## How a page scores in one synthetic run **`POST https://seolyticize.com/api/v1/tools/page_lab`** — How a page scores in one synthetic run Lighthouse performance, SEO, accessibility and best-practices scores for one address, from PageSpeed Insights. Answers 'what is slow or wrong on this page' with a reproducible lab run, including for pages too quiet for field data. This is not what real visitors experienced — use page_vitals for that, and say which is which when reporting both. There is no interaction metric here: a synthetic run interacts with nothing. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | | | `strategy` | string | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "url": "string", "strategy": "mobile", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/page_lab" \ -H "Content-Type: application/json" \ -d '{"url":"string","strategy":"mobile","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/page_lab', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"url":"string","strategy":"mobile","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/page_lab", headers={ "Content-Type": "application/json", }, json={"url": "string", "strategy": "mobile", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/page_lab", strings.NewReader(`{"url":"string","strategy":"mobile","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # How many people looked a brand or topic up ## How many people looked a brand or topic up **`POST https://seolyticize.com/api/v1/tools/get_entity_pageviews`** — How many people looked a brand or topic up Wikipedia article views for a brand or topic over a period, daily or monthly. Answers 'is interest in this name growing' with a complete count published by Wikimedia rather than a vendor's model or a panel sample. Returns nothing at all when Wikipedia has no article, which is the case for most small brands — and says which article it counted, because the closest article is not always the brand's own. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `entity` | string | yes | | | `language` | string | no | | | `granularity` | string | no | | | `from` | string | yes | | | `to` | string | yes | | ```json { "entity": "string", "language": "en", "granularity": "monthly", "from": "string", "to": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_entity_pageviews" \ -H "Content-Type: application/json" \ -d '{"entity":"string","language":"en","granularity":"monthly","from":"string","to":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_entity_pageviews', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"entity":"string","language":"en","granularity":"monthly","from":"string","to":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_entity_pageviews", headers={ "Content-Type": "application/json", }, json={"entity": "string", "language": "en", "granularity": "monthly", "from": "string", "to": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_entity_pageviews", strings.NewReader(`{"entity":"string","language":"en","granularity":"monthly","from":"string","to":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What the open web says about a domain ## What the open web says about a domain **`POST https://seolyticize.com/api/v1/tools/get_domain_facts`** — What the open web says about a domain Registration date and registrar from the registry, position on the Tranco top-sites list, when the Internet Archive first captured it, and the subdomains its certificates have announced. Answers 'how old and how big is this, and what else is on it' for any domain, free and without touching a vendor. Each figure is absent far more often than present, and an absence is never a zero: no registry record, not in the top million, never archived. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `domain` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "domain": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_domain_facts" \ -H "Content-Type: application/json" \ -d '{"domain":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_domain_facts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"domain":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_domain_facts", headers={ "Content-Type": "application/json", }, json={"domain": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_domain_facts", strings.NewReader(`{"domain":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Why a keyword is as hard as it is ## Why a keyword is as hard as it is **`POST https://seolyticize.com/api/v1/tools/explain_keyword_difficulty`** — Why a keyword is as hard as it is Which first-page results set a keyword's difficulty score and which make it harder than the score suggests: each result's referring domains and role (sets the median, above it, below it, or unscored for lack of link data), the strongest and weakest result, the domain authority beside each, and a one-paragraph summary. Answers 'why is this keyword a 47' so the number can be checked rather than trusted. Read from the archive and our link graph; buys nothing, and a keyword never captured is reported unscored. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `term` | string | yes | | | `locationCode` | integer | no | | | `languageCode` | string | no | | | `keywordId` | string | no | | ```json { "term": "string", "locationCode": 2840, "languageCode": "en", "keywordId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/explain_keyword_difficulty" \ -H "Content-Type: application/json" \ -d '{"term":"string","locationCode":2840,"languageCode":"en","keywordId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/explain_keyword_difficulty', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"term":"string","locationCode":2840,"languageCode":"en","keywordId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/explain_keyword_difficulty", headers={ "Content-Type": "application/json", }, json={"term": "string", "locationCode": 2840, "languageCode": "en", "keywordId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/explain_keyword_difficulty", strings.NewReader(`{"term":"string","locationCode":2840,"languageCode":"en","keywordId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List keyword lists ## List keyword lists **`POST https://seolyticize.com/api/v1/tools/list_keyword_lists`** — List keyword lists The keyword lists the caller can see, with how many keywords each holds — one client's, or every list the caller may reach when no client is named. Answers 'where did we save those keywords'. The agency's own lists, which belong to no client, are only visible to people who can see every client. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | no | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_keyword_lists" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_keyword_lists', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_keyword_lists", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_keyword_lists", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a keyword list ## Create a keyword list **`POST https://seolyticize.com/api/v1/tools/create_keyword_list`** — Create a keyword list Creates a named keyword list, for one client or for the agency, optionally with its first keywords. Answers 'keep these keywords somewhere'. A keyword is its words plus a location code, language and engine; the same words for another country are a different keyword. Returns created: false with the reason when the name is already taken, rather than failing. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `clientId` | string | no | | | `keywords` | object[] | no | | | `keywords.term` | string | yes | | | `keywords.locationCode` | integer | no | | | `keywords.languageCode` | string | no | | | `keywords.engine` | string | no | | ```json { "name": "string", "clientId": null, "keywords": [] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_keyword_list" \ -H "Content-Type: application/json" \ -d '{"name":"string","clientId":null,"keywords":[]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_keyword_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"name":"string","clientId":null,"keywords":[]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_keyword_list", headers={ "Content-Type": "application/json", }, json={"name": "string", "clientId": None, "keywords": []}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_keyword_list", strings.NewReader(`{"name":"string","clientId":null,"keywords":[]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Add keywords to a list ## Add keywords to a list **`POST https://seolyticize.com/api/v1/tools/add_keywords_to_list`** — Add keywords to a list Adds keywords to an existing list. Keywords already on it are left alone and counted as already present. Answers 'save these to that list'. Location code defaults to 2840 (United States), language to en, engine to google. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `listId` | string | yes | | | `keywords` | object[] | yes | | | `keywords.term` | string | yes | | | `keywords.locationCode` | integer | no | | | `keywords.languageCode` | string | no | | | `keywords.engine` | string | no | | ```json { "listId": "string", "keywords": [ { "term": "string", "locationCode": 2840, "languageCode": "en", "engine": "google" } ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_keywords_to_list" \ -H "Content-Type: application/json" \ -d '{"listId":"string","keywords":[{"term":"string","locationCode":2840,"languageCode":"en","engine":"google"}]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_keywords_to_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"listId":"string","keywords":[{"term":"string","locationCode":2840,"languageCode":"en","engine":"google"}]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_keywords_to_list", headers={ "Content-Type": "application/json", }, json={"listId": "string", "keywords": [{"term": "string", "locationCode": 2840, "languageCode": "en", "engine": "google"}]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_keywords_to_list", strings.NewReader(`{"listId":"string","keywords":[{"term":"string","locationCode":2840,"languageCode":"en","engine":"google"}]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Remove keywords from a list ## Remove keywords from a list **`POST https://seolyticize.com/api/v1/tools/remove_keywords_from_list`** — Remove keywords from a list Removes keywords from a list by keyword id. Tracked keywords that came from the list stay tracked. Answers 'take these off that list'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `listId` | string | yes | | | `keywordIds` | string[] | yes | | ```json { "listId": "string", "keywordIds": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/remove_keywords_from_list" \ -H "Content-Type: application/json" \ -d '{"listId":"string","keywordIds":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/remove_keywords_from_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"listId":"string","keywordIds":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/remove_keywords_from_list", headers={ "Content-Type": "application/json", }, json={"listId": "string", "keywordIds": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/remove_keywords_from_list", strings.NewReader(`{"listId":"string","keywordIds":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List report templates ## List report templates **`POST https://seolyticize.com/api/v1/tools/list_report_templates`** — List report templates Lists the report templates that apply: the agency's own, plus the client's when clientId is given — each with its audience, sections in order with guidance, tone and sign-off. Answers 'how do we usually write this report'. Follow a template's sections and tone when writing a report it fits, and name it in save_written_report. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | no | | ```json { "clientId": null } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_report_templates" \ -H "Content-Type: application/json" \ -d '{"clientId":null}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_report_templates', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":null}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_report_templates", headers={ "Content-Type": "application/json", }, json={"clientId": None}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_report_templates", strings.NewReader(`{"clientId":null}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Save a report template ## Save a report template **`POST https://seolyticize.com/api/v1/tools/save_report_template`** — Save a report template Saves a reusable brief for a kind of report: a name, who it is for, sections in order (each with a line on what goes under it), tone and sign-off. Answers 'make this our monthly report format'. Without clientId it is agency-wide, which only someone who sees every client may change; with one it is that client's own. Without templateId a new template is made; with one, it is replaced. Only save a template when the user asks for one — a one-off report needs none. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `templateId` | string | no | | | `clientId` | string | no | | | `name` | string | yes | | | `description` | string | no | | | `audience` | string | no | | | `sections` | object[] | yes | | | `sections.heading` | string | yes | | | `sections.guidance` | string | no | | | `tone` | string | no | | | `signOff` | string | no | | ```json { "templateId": "string", "clientId": null, "name": "string", "description": "string", "audience": "string", "sections": [ { "heading": "string", "guidance": "string" } ], "tone": "string", "signOff": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/save_report_template" \ -H "Content-Type: application/json" \ -d '{"templateId":"string","clientId":null,"name":"string","description":"string","audience":"string","sections":[{"heading":"string","guidance":"string"}],"tone":"string","signOff":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/save_report_template', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"templateId":"string","clientId":null,"name":"string","description":"string","audience":"string","sections":[{"heading":"string","guidance":"string"}],"tone":"string","signOff":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/save_report_template", headers={ "Content-Type": "application/json", }, json={"templateId": "string", "clientId": None, "name": "string", "description": "string", "audience": "string", "sections": [{"heading": "string", "guidance": "string"}], "tone": "string", "signOff": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/save_report_template", strings.NewReader(`{"templateId":"string","clientId":null,"name":"string","description":"string","audience":"string","sections":[{"heading":"string","guidance":"string"}],"tone":"string","signOff":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Delete a report template ## Delete a report template **`POST https://seolyticize.com/api/v1/tools/delete_report_template`** — Delete a report template Permanently deletes one report template. Answers 'we no longer use that format'. Reports written to it are kept. Call list_report_templates to find its id. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `templateId` | string | yes | | | `clientId` | string | no | | ```json { "templateId": "string", "clientId": null } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_report_template" \ -H "Content-Type: application/json" \ -d '{"templateId":"string","clientId":null}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_report_template', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"templateId":"string","clientId":null}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_report_template", headers={ "Content-Type": "application/json", }, json={"templateId": "string", "clientId": None}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_report_template", strings.NewReader(`{"templateId":"string","clientId":null}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Ask assistants one question now ## Ask assistants one question now **`POST https://seolyticize.com/api/v1/tools/explore_ai_prompt`** — Ask assistants one question now Asks one question live on the chosen assistants (claude, and any the agency has its own API key for) and returns each answer's text, citations, and — when clientId is given — which tracked brands it named. Nothing is stored; use it to see what assistants say before tracking a question, or to look up how they describe a brand. Answers 'what does ChatGPT say about this'. Claude is metered (meteredMicros); others bill the agency's own accounts. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | no | | | `prompt` | string | yes | | | `engines` | string[] | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": null, "prompt": "string", "engines": [ "claude" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/explore_ai_prompt" \ -H "Content-Type: application/json" \ -d '{"clientId":null,"prompt":"string","engines":["claude"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/explore_ai_prompt', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":null,"prompt":"string","engines":["claude"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/explore_ai_prompt", headers={ "Content-Type": "application/json", }, json={"clientId": None, "prompt": "string", "engines": ["claude"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/explore_ai_prompt", strings.NewReader(`{"clientId":null,"prompt":"string","engines":["claude"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Find a market's location code ## Find a market's location code **`POST https://seolyticize.com/api/v1/tools/list_markets`** — Find a market's location code Lists the countries and regions this product measures, with the location code other tools take (locationCode), the ISO code and Google's own country code. Search by name, ISO code (gb), Google code (uk) or location code (2826); an exact code wins over a name match. Answers 'what is the location code for Germany'. Only markets the product can measure honestly are listed. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | no | | ```json { "query": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_markets" \ -H "Content-Type: application/json" \ -d '{"query":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_markets', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"query":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_markets", headers={ "Content-Type": "application/json", }, json={"query": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_markets", strings.NewReader(`{"query":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read a keyword list ## Read a keyword list **`POST https://seolyticize.com/api/v1/tools/get_keyword_list`** — Read a keyword list Every keyword in one list (by listId from list_keyword_lists): the term, its market, language and engine, its keywordId, and its note. Answers 'what is on this list'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `listId` | string | yes | | ```json { "listId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_keyword_list" \ -H "Content-Type: application/json" \ -d '{"listId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_keyword_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"listId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_keyword_list", headers={ "Content-Type": "application/json", }, json={"listId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_keyword_list", strings.NewReader(`{"listId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Note why keywords are on a list ## Note why keywords are on a list **`POST https://seolyticize.com/api/v1/tools/set_keyword_notes`** — Note why keywords are on a list Sets notes on keywords in a list — why each is there, which page it belongs to, what was decided — by keywordId from get_keyword_list. An empty note clears it; ids not on the list are ignored and changed says how many were set. Answers 'note that this one is for the implants page'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `listId` | string | yes | | | `notes` | object[] | yes | | | `notes.keywordId` | string | yes | | | `notes.note` | string | yes | | ```json { "listId": "string", "notes": [ { "keywordId": "string", "note": "string" } ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/set_keyword_notes" \ -H "Content-Type: application/json" \ -d '{"listId":"string","notes":[{"keywordId":"string","note":"string"}]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/set_keyword_notes', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"listId":"string","notes":[{"keywordId":"string","note":"string"}]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/set_keyword_notes", headers={ "Content-Type": "application/json", }, json={"listId": "string", "notes": [{"keywordId": "string", "note": "string"}]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/set_keyword_notes", strings.NewReader(`{"listId":"string","notes":[{"keywordId":"string","note":"string"}]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Who am I connected as ## Who am I connected as **`POST https://seolyticize.com/api/v1/tools/whoami`** — Who am I connected as Says who this connection is: the agency, whether it is an API key or a person, the role, every permission that role holds, and the clients it can reach. Answers 'what can this key do'. Call it first when unsure whether a task is allowed. Free. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/whoami" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/whoami', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/whoami", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/whoami", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a client ## Create a client **`POST https://seolyticize.com/api/v1/tools/create_client`** — Create a client Onboards a client in one step: the client, a project, its site and a rank tracker, optionally with keywords to track. Answers 'set up a new client'. Refused when the name is taken or the plan's client or keyword allowance is used up; every tracked keyword is a paid lookup on every run. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `domain` | string | yes | | | `keywords` | string[] | no | | | `monthlyBudgetUsd` | number | no | | ```json { "name": "string", "domain": "string", "keywords": [ "string" ], "monthlyBudgetUsd": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_client" \ -H "Content-Type: application/json" \ -d '{"name":"string","domain":"string","keywords":["string"],"monthlyBudgetUsd":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_client', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"name":"string","domain":"string","keywords":["string"],"monthlyBudgetUsd":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_client", headers={ "Content-Type": "application/json", }, json={"name": "string", "domain": "string", "keywords": ["string"], "monthlyBudgetUsd": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_client", strings.NewReader(`{"name":"string","domain":"string","keywords":["string"],"monthlyBudgetUsd":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Which vendor accounts are connected ## Which vendor accounts are connected **`POST https://seolyticize.com/api/v1/tools/get_vendor_connections`** — Which vendor accounts are connected Which data vendors and AI assistants the agency has connected its own account for, and whether each last checked out. Answers 'why is this data missing' when a vendor was never connected. Never returns any part of a credential. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_vendor_connections" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_vendor_connections', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_vendor_connections", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_vendor_connections", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Vendor spend by client ## Vendor spend by client **`POST https://seolyticize.com/api/v1/tools/get_costs`** — Vendor spend by client What the agency spent on data vendors per client over recent days, with request counts. Answers 'which clients cost us the most'. Amounts are US dollars. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `sinceDays` | integer | no | | ```json { "sinceDays": 30 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_costs" \ -H "Content-Type: application/json" \ -d '{"sinceDays":30}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_costs', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"sinceDays":30}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_costs", headers={ "Content-Type": "application/json", }, json={"sinceDays": 30}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_costs", strings.NewReader(`{"sinceDays":30}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List API keys ## List API keys **`POST https://seolyticize.com/api/v1/tools/list_api_keys`** — List API keys The agency's live API keys with name, role, which client each is pinned to and when it was last used. Lists keys to audit or revoke. Never returns a key. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_api_keys" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_api_keys', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_api_keys", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_api_keys", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create an API key ## Create an API key **`POST https://seolyticize.com/api/v1/tools/create_api_key`** — Create an API key Mints an API key, agency-wide or pinned to one client, with a role: client_viewer (read-only), analyst, manager, site_admin or admin. Answers 'give this integration its own key'. For an agent that runs one site, pin it to that client with site_admin: full control of that client — connecting consoles, crawls, onboarding — and none of the agency's settings, team, keys or billing; the key cannot even see other clients. A client-pinned key is at most site_admin, and site_admin needs a clientId. expiresInDays makes it stop working on its own; maxCostUsdPerCall caps what any one call with it may spend. The key is in the response once and can never be shown again. A key cannot be given more access than the caller has. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `clientId` | string | no | | | `role` | string | no | | | `expiresInDays` | integer | no | | | `maxCostUsdPerCall` | number | no | The most any one call with this key may spend, in USD. Calls asking for more are capped to it. | ```json { "name": "string", "clientId": "string", "role": "manager", "expiresInDays": 0, "maxCostUsdPerCall": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_api_key" \ -H "Content-Type: application/json" \ -d '{"name":"string","clientId":"string","role":"manager","expiresInDays":0,"maxCostUsdPerCall":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_api_key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"name":"string","clientId":"string","role":"manager","expiresInDays":0,"maxCostUsdPerCall":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_api_key", headers={ "Content-Type": "application/json", }, json={"name": "string", "clientId": "string", "role": "manager", "expiresInDays": 0, "maxCostUsdPerCall": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_api_key", strings.NewReader(`{"name":"string","clientId":"string","role":"manager","expiresInDays":0,"maxCostUsdPerCall":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Revoke an API key ## Revoke an API key **`POST https://seolyticize.com/api/v1/tools/revoke_api_key`** — Revoke an API key Stops a key working, immediately and for good. Answers 'this key leaked'. Safe to repeat; revoking the key making the call is allowed and takes effect on its next request. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `keyId` | string | yes | | ```json { "keyId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/revoke_api_key" \ -H "Content-Type: application/json" \ -d '{"keyId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/revoke_api_key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"keyId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/revoke_api_key", headers={ "Content-Type": "application/json", }, json={"keyId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/revoke_api_key", strings.NewReader(`{"keyId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List API activity ## List API activity **`POST https://seolyticize.com/api/v1/tools/list_api_activity`** — List API activity Recent tool calls made with the agency's keys — which key, which tool, which client, how it ended and how long it took — newest first. Lists who did what through the API and MCP. Inputs are never recorded. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `sinceHours` | integer | no | | | `keyId` | string | no | | | `limit` | integer | no | | ```json { "sinceHours": 24, "keyId": "string", "limit": 200 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_api_activity" \ -H "Content-Type: application/json" \ -d '{"sinceHours":24,"keyId":"string","limit":200}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_api_activity', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"sinceHours":24,"keyId":"string","limit":200}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_api_activity", headers={ "Content-Type": "application/json", }, json={"sinceHours": 24, "keyId": "string", "limit": 200}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_api_activity", strings.NewReader(`{"sinceHours":24,"keyId":"string","limit":200}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List the team ## List the team **`POST https://seolyticize.com/api/v1/tools/list_team_members`** — List the team Everyone with access to the agency: email, name, role, and the clients each is limited to (empty means agency-wide). Also lists the clients a new member could be limited to. Answers 'who can see this account'. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_team_members" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_team_members', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_team_members", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_team_members", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Add someone to the team ## Add someone to the team **`POST https://seolyticize.com/api/v1/tools/add_team_member`** — Add someone to the team Gives an existing Seolyticize account access to the agency with a role — admin, manager, analyst or client_viewer — optionally limited to some clients. Answers 'give Sam access as an analyst'. The person must already have signed up; this never creates an account for them. Refused for someone already on the team, and for a client outside the caller's own reach. Owners can only be added by an owner, in the app. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | yes | | | `role` | string | yes | | | `clientIds` | string[] | no | | ```json { "email": "string", "role": "admin", "clientIds": [] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_team_member" \ -H "Content-Type: application/json" \ -d '{"email":"string","role":"admin","clientIds":[]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_team_member', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"email":"string","role":"admin","clientIds":[]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_team_member", headers={ "Content-Type": "application/json", }, json={"email": "string", "role": "admin", "clientIds": []}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_team_member", strings.NewReader(`{"email":"string","role":"admin","clientIds":[]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Remove someone from the team ## Remove someone from the team **`POST https://seolyticize.com/api/v1/tools/remove_team_member`** — Remove someone from the team Ends a member's access to the agency, by the membershipId list_team_members gives. Answers 'Sam has left'. Their account itself is untouched. The last owner cannot be removed. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `membershipId` | string | yes | | ```json { "membershipId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/remove_team_member" \ -H "Content-Type: application/json" \ -d '{"membershipId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/remove_team_member', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"membershipId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/remove_team_member", headers={ "Content-Type": "application/json", }, json={"membershipId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/remove_team_member", strings.NewReader(`{"membershipId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Connect a vendor account ## Connect a vendor account **`POST https://seolyticize.com/api/v1/tools/save_vendor_credential`** — Connect a vendor account Stores the agency's own account for a data vendor or AI assistant — DataForSEO (login + password), SerpApi, a proxy pool (serpproxy: one URL per line), Google Ads (six values in googleAds), an Anthropic, OpenAI, Gemini or Perplexity key, and the rest get_vendor_connections names — and checks it works straight away. Answers 'connect our DataForSEO account'. Replaces any key already saved for that vendor. The response says whether the check passed; it never contains the key. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `provider` | string | yes | | | `login` | string | no | | | `secret` | string | yes | | | `googleAds` | object | no | | | `googleAds.developerToken` | string | yes | | | `googleAds.customerId` | string | yes | | | `googleAds.loginCustomerId` | string | no | | | `googleAds.clientId` | string | yes | | | `googleAds.clientSecret` | string | yes | | | `googleAds.refreshToken` | string | yes | | ```json { "provider": "dataforseo", "login": "string", "secret": "string", "googleAds": { "developerToken": "string", "customerId": "string", "loginCustomerId": "string", "clientId": "string", "clientSecret": "string", "refreshToken": "string" } } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/save_vendor_credential" \ -H "Content-Type: application/json" \ -d '{"provider":"dataforseo","login":"string","secret":"string","googleAds":{"developerToken":"string","customerId":"string","loginCustomerId":"string","clientId":"string","clientSecret":"string","refreshToken":"string"}}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/save_vendor_credential', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"provider":"dataforseo","login":"string","secret":"string","googleAds":{"developerToken":"string","customerId":"string","loginCustomerId":"string","clientId":"string","clientSecret":"string","refreshToken":"string"}}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/save_vendor_credential", headers={ "Content-Type": "application/json", }, json={"provider": "dataforseo", "login": "string", "secret": "string", "googleAds": {"developerToken": "string", "customerId": "string", "loginCustomerId": "string", "clientId": "string", "clientSecret": "string", "refreshToken": "string"}}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/save_vendor_credential", strings.NewReader(`{"provider":"dataforseo","login":"string","secret":"string","googleAds":{"developerToken":"string","customerId":"string","loginCustomerId":"string","clientId":"string","clientSecret":"string","refreshToken":"string"}}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Disconnect a vendor account ## Disconnect a vendor account **`POST https://seolyticize.com/api/v1/tools/remove_vendor_credential`** — Disconnect a vendor account Deletes the agency's saved account for one vendor. Answers 'stop using our DataForSEO account'. Anything that relied on it — keyword data from DataForSEO, an assistant's answers — stops until another is saved. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `provider` | string | yes | | ```json { "provider": "dataforseo" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/remove_vendor_credential" \ -H "Content-Type: application/json" \ -d '{"provider":"dataforseo"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/remove_vendor_credential', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"provider":"dataforseo"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/remove_vendor_credential", headers={ "Content-Type": "application/json", }, json={"provider": "dataforseo"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/remove_vendor_credential", strings.NewReader(`{"provider":"dataforseo"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read the agency's branding ## Read the agency's branding **`POST https://seolyticize.com/api/v1/tools/get_branding`** — Read the agency's branding The agency's name, logo address and primary colour: what clients see on reports and portal links. Answers 'what will this report look like to the client'. Empty strings where nothing is set. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_branding" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_branding', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_branding", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_branding", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Set the agency's branding ## Set the agency's branding **`POST https://seolyticize.com/api/v1/tools/set_branding`** — Set the agency's branding Renames the agency and sets the logo and colour on its reports and portal links. Answers 'put our logo on the reports'. The logo must be an https:// address and the colour six-digit hex such as #1f5ad6; empty strings clear them. Send all three: this replaces what is there. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `logoUrl` | string | yes | | | `primaryColor` | string | yes | | ```json { "name": "string", "logoUrl": "string", "primaryColor": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/set_branding" \ -H "Content-Type: application/json" \ -d '{"name":"string","logoUrl":"string","primaryColor":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/set_branding', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"name":"string","logoUrl":"string","primaryColor":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/set_branding", headers={ "Content-Type": "application/json", }, json={"name": "string", "logoUrl": "string", "primaryColor": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/set_branding", strings.NewReader(`{"name":"string","logoUrl":"string","primaryColor":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List connected apps ## List connected apps **`POST https://seolyticize.com/api/v1/tools/list_connected_apps`** — List connected apps Assistants and agent apps connected to the agency by signing in (MCP OAuth) rather than with an API key: which app, the role and client it was given, who approved it, and when it was last used. Answers 'what has access to our account'. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_connected_apps" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_connected_apps', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_connected_apps", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_connected_apps", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Disconnect an app ## Disconnect an app **`POST https://seolyticize.com/api/v1/tools/disconnect_app`** — Disconnect an app Stops a signed-in app working, at once: its access and refresh tokens both end. Answers 'remove that assistant's access'. Takes the grantId from list_connected_apps. The app can only come back if somebody signs in and approves it again. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `grantId` | string | yes | | ```json { "grantId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/disconnect_app" \ -H "Content-Type: application/json" \ -d '{"grantId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/disconnect_app', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"grantId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/disconnect_app", headers={ "Content-Type": "application/json", }, json={"grantId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/disconnect_app", strings.NewReader(`{"grantId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read the agency's plan ## Read the agency's plan **`POST https://seolyticize.com/api/v1/tools/get_billing`** — Read the agency's plan The agency's plan, its limits, how many clients it uses, and the subscription's status and renewal date. Answers 'how many more clients can we add'. `configured` is false where this deployment sells nothing — every agency is on the trial plan then, and that is not an error. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_billing" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_billing', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_billing", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_billing", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get a link to pay or manage the plan ## Get a link to pay or manage the plan **`POST https://seolyticize.com/api/v1/tools/get_billing_link`** — Get a link to pay or manage the plan A Stripe address for a person to open: checkout for a plan (studio, agency or network), or — with no plan, for an agency already paying — the portal where cards, invoices and cancellation live. Answers 'upgrade our plan'. An agent cannot pay; hand the address to a person. Refused where this deployment has no billing set up. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `plan` | string | no | | ```json { "plan": "studio" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_billing_link" \ -H "Content-Type: application/json" \ -d '{"plan":"studio"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_billing_link', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"plan":"studio"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_billing_link", headers={ "Content-Type": "application/json", }, json={"plan": "studio"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_billing_link", strings.NewReader(`{"plan":"studio"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get the agency's Google service account ## Get the agency's Google service account **`POST https://seolyticize.com/api/v1/tools/get_google_service_account`** — Get the agency's Google service account The email address of the agency's own Google service account, made the first time it is asked for. Answers 'what do I add in Search Console so we can connect without signing in'. A client's property owner adds this address as a user — Search Console: Settings → Users and permissions, Restricted; GA4: Admin → Property access management, Viewer — and then connect_search_console_service_account or connect_google_analytics_service_account connects it with no Google sign-in. The address is the agency's alone; give it to your human to pass on. Free. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_google_service_account" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_google_service_account', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_google_service_account", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_google_service_account", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Onboard a site in one call ## Onboard a site in one call **`POST https://seolyticize.com/api/v1/tools/onboard_site`** — Onboard a site in one call Answers 'set this site up'. Makes the client, project, site and rank tracker — or finds the client already here for this domain, so calling it twice is safe — issues the IndexNow key and a Google site verification for the agency's own Google account, sets up cookieless analytics, and returns one deploy manifest: files to serve at exact paths, tags for the , the analytics snippet placed for the framework given, and CSP entries. Deploy all of it, then call verify_site. No Google sign-in or Search Console settings are needed at any point. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `domain` | string | yes | | | `name` | string | no | | | `framework` | string | no | | | `keywords` | string[] | no | | | `verification` | string | no | file (a page at the site root, the default), meta (a tag in the ) or dns (a TXT record, which also covers every subdomain and protocol). | ```json { "domain": "string", "name": "string", "framework": "nextjs", "keywords": [ "string" ], "verification": "file" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/onboard_site" \ -H "Content-Type: application/json" \ -d '{"domain":"string","name":"string","framework":"nextjs","keywords":["string"],"verification":"file"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/onboard_site', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"domain":"string","name":"string","framework":"nextjs","keywords":["string"],"verification":"file"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/onboard_site", headers={ "Content-Type": "application/json", }, json={"domain": "string", "name": "string", "framework": "nextjs", "keywords": ["string"], "verification": "file"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/onboard_site", strings.NewReader(`{"domain":"string","name":"string","framework":"nextjs","keywords":["string"],"verification":"file"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create an invite code for a site's agent ## Create an invite code for a site's agent **`POST https://seolyticize.com/api/v1/tools/create_invite_code`** — Create an invite code for a site's agent Answers 'let an agent connect its own site without our admin key'. Makes a code to hand to the agent that runs one site. The agent sends it, with its domain, to POST /api/v1/claim (no key needed) and gets back a key pinned to the one new client for that site, with the site_admin role: full control of that client, nothing else in the agency. The code is in this response once. maxUses (default 1) is how many sites it can claim; expiresInDays ends the code, keyExpiresInDays the keys it makes, and keyMaxCostUsdPerCall caps what any one call with those keys may spend. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | string | yes | | | `maxUses` | integer | no | | | `expiresInDays` | integer | no | | | `keyExpiresInDays` | integer | no | | | `keyMaxCostUsdPerCall` | number | no | | ```json { "label": "string", "maxUses": 1, "expiresInDays": 0, "keyExpiresInDays": 0, "keyMaxCostUsdPerCall": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_invite_code" \ -H "Content-Type: application/json" \ -d '{"label":"string","maxUses":1,"expiresInDays":0,"keyExpiresInDays":0,"keyMaxCostUsdPerCall":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_invite_code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"label":"string","maxUses":1,"expiresInDays":0,"keyExpiresInDays":0,"keyMaxCostUsdPerCall":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_invite_code", headers={ "Content-Type": "application/json", }, json={"label": "string", "maxUses": 1, "expiresInDays": 0, "keyExpiresInDays": 0, "keyMaxCostUsdPerCall": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_invite_code", strings.NewReader(`{"label":"string","maxUses":1,"expiresInDays":0,"keyExpiresInDays":0,"keyMaxCostUsdPerCall":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List invite codes ## List invite codes **`POST https://seolyticize.com/api/v1/tools/list_invite_codes`** — List invite codes Lists the agency's invite codes — label, uses left, expiry, whether revoked — never the codes themselves. Answers 'which invite codes are out there'. Free. ### Request body (`application/json`) ```json {} ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_invite_codes" \ -H "Content-Type: application/json" \ -d '{}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_invite_codes', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_invite_codes", headers={ "Content-Type": "application/json", }, json={}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_invite_codes", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Revoke an invite code ## Revoke an invite code **`POST https://seolyticize.com/api/v1/tools/revoke_invite_code`** — Revoke an invite code Stops an invite code being claimed again. Answers 'that code was shared too widely'. Keys already made from it keep working until revoked with revoke_api_key. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `inviteCodeId` | string | yes | | ```json { "inviteCodeId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/revoke_invite_code" \ -H "Content-Type: application/json" \ -d '{"inviteCodeId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/revoke_invite_code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"inviteCodeId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/revoke_invite_code", headers={ "Content-Type": "application/json", }, json={"inviteCodeId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/revoke_invite_code", strings.NewReader(`{"inviteCodeId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # See what a domain is built with ## See what a domain is built with **`POST https://seolyticize.com/api/v1/tools/get_domain_technologies`** — See what a domain is built with The technologies our own crawler detected on any domain — CMS, analytics, commerce, CDN and so on — with the version most pages showed and how many pages showed it, as of the day it was last crawled. Answers 'what is this site built on'. Free. Null when our crawler has never fetched the domain, which says nothing about what it runs. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `domain` | string | yes | | ```json { "domain": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_domain_technologies" \ -H "Content-Type: application/json" \ -d '{"domain":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_domain_technologies', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"domain":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_domain_technologies", headers={ "Content-Type": "application/json", }, json={"domain": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_domain_technologies", strings.NewReader(`{"domain":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Rename a keyword list ## Rename a keyword list **`POST https://seolyticize.com/api/v1/tools/rename_keyword_list`** — Rename a keyword list Gives a keyword list a new name. Answers 'call that list something else'. Refused when another list for the same client (or the agency) already has the name. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `listId` | string | yes | | | `name` | string | yes | | ```json { "listId": "string", "name": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/rename_keyword_list" \ -H "Content-Type: application/json" \ -d '{"listId":"string","name":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/rename_keyword_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"listId":"string","name":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/rename_keyword_list", headers={ "Content-Type": "application/json", }, json={"listId": "string", "name": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/rename_keyword_list", strings.NewReader(`{"listId":"string","name":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Delete a keyword list ## Delete a keyword list **`POST https://seolyticize.com/api/v1/tools/delete_keyword_list`** — Delete a keyword list Deletes a keyword list and the keywords on it, for good. Answers 'get rid of that list'. Keywords a tracker took from the list stay tracked. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `listId` | string | yes | | ```json { "listId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_keyword_list" \ -H "Content-Type: application/json" \ -d '{"listId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_keyword_list', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"listId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_keyword_list", headers={ "Content-Type": "application/json", }, json={"listId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_keyword_list", strings.NewReader(`{"listId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Build a disavow file ## Build a disavow file **`POST https://seolyticize.com/api/v1/tools/build_disavow_file`** — Build a disavow file Builds a Google disavow file from domains and URLs: a domain entry disavows every link from that domain, a url entry names one page. Answers 'make the disavow file for these links'. Free; nothing is submitted — Google takes the file by upload in Search Console, which a person does. Use get_backlink_audit to find the links worth disavowing, and have a person review every line first: disavowing a good link removes value the client earned. Malformed entries are left out rather than written. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `entries` | object[] | yes | | | `entries.sourceDomain` | string | yes | | | `entries.scope` | string | yes | | | `entries.url` | string | no | | ```json { "entries": [ { "sourceDomain": "string", "scope": "domain", "url": "string" } ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/build_disavow_file" \ -H "Content-Type: application/json" \ -d '{"entries":[{"sourceDomain":"string","scope":"domain","url":"string"}]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/build_disavow_file', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"entries":[{"sourceDomain":"string","scope":"domain","url":"string"}]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/build_disavow_file", headers={ "Content-Type": "application/json", }, json={"entries": [{"sourceDomain": "string", "scope": "domain", "url": "string"}]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/build_disavow_file", strings.NewReader(`{"entries":[{"sourceDomain":"string","scope":"domain","url":"string"}]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Estimate what a call will cost ## Estimate what a call will cost **`POST https://seolyticize.com/api/v1/tools/estimate_cost`** — Estimate what a call will cost Quotes the most a tool call can cost before it is made, without buying anything. Answers 'how much will this cost'. Pass the tool's name and the exact arguments you would call it with; the result lists each purchase the call can make with its ceiling, plus monthlyMicros for scheduled work. Show the estimate to the user before a paid call, then pass estimatedMaxMicros as that call's maxCostMicros so it cannot spend more. Cached data costs nothing, so real charges are often lower. Also quotes "rank_tracker": a rank tracker's captures (arguments: clientId, trackerId) — per run and per month. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `tool` | string | yes | | | `arguments` | object | no | | ```json { "tool": "string", "arguments": {} } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/estimate_cost" \ -H "Content-Type: application/json" \ -d '{"tool":"string","arguments":{}}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/estimate_cost', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"tool":"string","arguments":{}}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/estimate_cost", headers={ "Content-Type": "application/json", }, json={"tool": "string", "arguments": {}}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/estimate_cost", strings.NewReader(`{"tool":"string","arguments":{}}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Client Client --- # Get rank summary ## Get rank summary **`POST https://seolyticize.com/api/v1/tools/get_rank_summary`** — Get rank summary How many tracked keywords improved, declined or held for a client, with the average position. Answers 'are rankings getting better'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | ```json { "clientId": "string", "trackerId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_rank_summary" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_rank_summary', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_rank_summary", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_rank_summary", strings.NewReader(`{"clientId":"string","trackerId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get share of voice ## Get share of voice **`POST https://seolyticize.com/api/v1/tools/get_share_of_voice`** — Get share of voice The client's share of available search clicks across its tracked keywords, alongside competitors. Answers 'how much of the market do we hold'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | ```json { "clientId": "string", "trackerId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_share_of_voice" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_share_of_voice', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_share_of_voice", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_share_of_voice", strings.NewReader(`{"clientId":"string","trackerId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Submit URLs for crawling ## Submit URLs for crawling **`POST https://seolyticize.com/api/v1/tools/submit_urls`** — Submit URLs for crawling Adds addresses to the crawl frontier for a client. Only accepts addresses on a site the agency manages — anything else is refused and named. Answers 'crawl these pages I found'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `urls` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "urls": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/submit_urls" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","urls":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/submit_urls', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","urls":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/submit_urls", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "urls": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/submit_urls", strings.NewReader(`{"clientId":"string","urls":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List recent alerts ## List recent alerts **`POST https://seolyticize.com/api/v1/tools/list_recent_alerts`** — List recent alerts Alerts raised for a client since a given time, newest first. Lists what has fired, for anything watching — a chat channel, an automation, a status board. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `since` | string | yes | | ```json { "clientId": "string", "since": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_recent_alerts" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","since":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_recent_alerts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","since":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_recent_alerts", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "since": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_recent_alerts", strings.NewReader(`{"clientId":"string","since":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get keyword difficulty ## Get keyword difficulty **`POST https://seolyticize.com/api/v1/tools/get_keyword_difficulty`** — Get keyword difficulty How hard the first page looks for given keywords, scored from how many domains link to the pages already ranking there. Answers 'is this term worth going after'. Null where too few results have link data to judge. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `keywordIds` | string[] | yes | | ```json { "clientId": "string", "keywordIds": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_keyword_difficulty" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","keywordIds":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_keyword_difficulty', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","keywordIds":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_keyword_difficulty", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "keywordIds": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_keyword_difficulty", strings.NewReader(`{"clientId":"string","keywordIds":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get keyword gap ## Get keyword gap **`POST https://seolyticize.com/api/v1/tools/get_keyword_gap`** — Get keyword gap Keywords competitors rank for that the client does not, sized by the clicks available. Answers 'which terms are we missing'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `competitorDomains` | string[] | yes | | ```json { "clientId": "string", "competitorDomains": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_keyword_gap" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","competitorDomains":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_keyword_gap', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","competitorDomains":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_keyword_gap", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "competitorDomains": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_keyword_gap", strings.NewReader(`{"clientId":"string","competitorDomains":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get content gap ## Get content gap **`POST https://seolyticize.com/api/v1/tools/get_content_gap`** — Get content gap Competitor pages that own a cluster of keywords the client does not cover. Answers 'what page should we build next' — use this rather than keyword gap when planning content. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `competitorDomains` | string[] | yes | | ```json { "clientId": "string", "competitorDomains": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_content_gap" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","competitorDomains":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_content_gap', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","competitorDomains":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_content_gap", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "competitorDomains": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_content_gap", strings.NewReader(`{"clientId":"string","competitorDomains":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get link prospects ## Get link prospects **`POST https://seolyticize.com/api/v1/tools/get_link_intersect`** — Get link prospects Sites linking to the client's competitors but not to the client. Answers 'who should we approach for links'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `competitorDomains` | string[] | yes | | ```json { "clientId": "string", "competitorDomains": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_link_intersect" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","competitorDomains":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_link_intersect', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","competitorDomains":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_link_intersect", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "competitorDomains": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_link_intersect", strings.NewReader(`{"clientId":"string","competitorDomains":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get cannibalization ## Get cannibalization **`POST https://seolyticize.com/api/v1/tools/get_cannibalization`** — Get cannibalization Keywords where several of the client's own pages compete in the same result. Answers 'where are we competing with ourselves'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_cannibalization" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_cannibalization', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_cannibalization", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_cannibalization", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get decaying content ## Get decaying content **`POST https://seolyticize.com/api/v1/tools/get_content_decay`** — Get decaying content Pages losing the traffic they used to earn, over consecutive periods. Answers 'what should we refresh'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_content_decay" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_content_decay', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_content_decay", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_content_decay", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get site audit summary ## Get site audit summary **`POST https://seolyticize.com/api/v1/tools/get_audit_summary`** — Get site audit summary Technical issues found by the most recent crawl, grouped by rule. Answers 'what is broken on the site'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `crawlId` | string | yes | | ```json { "clientId": "string", "crawlId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_audit_summary" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","crawlId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_audit_summary', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","crawlId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_audit_summary", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "crawlId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_audit_summary", strings.NewReader(`{"clientId":"string","crawlId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get AI assistant visibility ## Get AI assistant visibility **`POST https://seolyticize.com/api/v1/tools/get_ai_visibility`** — Get AI assistant visibility How often the client's brand appears in AI assistant answers versus competitors, and what kind of pages those answers were built from. Answers 'are we visible in ChatGPT, Copilot, Gemini and Claude' and 'what do we have to be on to get cited'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ai_visibility" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ai_visibility', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get agent readiness ## Get agent readiness **`POST https://seolyticize.com/api/v1/tools/get_agent_readiness`** — Get agent readiness Whether the client's site can be read and cited by AI assistants — crawler access, rendering, structured data. Answers 'why are assistants not citing us'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_agent_readiness" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_agent_readiness', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_agent_readiness", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_agent_readiness", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Tell search engines a page changed ## Tell search engines a page changed **`POST https://seolyticize.com/api/v1/tools/submit_indexnow`** — Tell search engines a page changed Submits changed addresses to IndexNow, which Bing, Yandex, Seznam and Naver share. Google does not take part. Only accepts addresses on a site the agency manages — anything else is refused and named. Answers 'we have just republished these pages, tell the engines'. Refuses entirely until the client's key file has been published and checked. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `urls` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "urls": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/submit_indexnow" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","urls":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/submit_indexnow', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","urls":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/submit_indexnow", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "urls": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/submit_indexnow", strings.NewReader(`{"clientId":"string","urls":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Explore keyword ideas with filters ## Explore keyword ideas with filters **`POST https://seolyticize.com/api/v1/tools/keyword_explorer`** — Explore keyword ideas with filters Expands a seed term into keyword ideas, suggestions, related terms or questions, then filters, sorts and pages them: include and exclude words, volume, difficulty, cost per click and word-count ranges, questions only, search intent, and result-page features. Answers 'which terms around this topic are worth going after'. The expansion is bought once per seed, mode and market; every later filter on the same seed is free. Rows a filter cannot judge because the number is unknown are left out and counted in unknownExcluded rather than treated as zero. Set format to csv for every matching row as a CSV file. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `seed` | string | yes | | | `mode` | string | no | | | `locationCode` | integer | no | | | `languageCode` | string | no | | | `filters` | object | no | | | `filters.include` | string[] | no | | | `filters.includeMode` | string | no | | | `filters.exclude` | string[] | no | | | `filters.volumeMin` | number | no | | | `filters.volumeMax` | number | no | | | `filters.difficultyMin` | number | no | | | `filters.difficultyMax` | number | no | | | `filters.cpcMinUsd` | number | no | | | `filters.cpcMaxUsd` | number | no | | | `filters.wordsMin` | integer | no | | | `filters.wordsMax` | integer | no | | | `filters.questionsOnly` | boolean | no | | | `filters.intents` | string[] | no | | | `filters.serpFeatures` | string[] | no | | | `sort` | object | no | | | `sort.key` | string | yes | | | `sort.direction` | string | no | | | `page` | integer | no | | | `pageSize` | integer | no | | | `format` | string | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "seed": "string", "mode": "ideas", "locationCode": 2840, "languageCode": "en", "filters": {}, "sort": { "key": "volume", "direction": "desc" }, "page": 1, "pageSize": 100, "format": "rows", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/keyword_explorer" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","seed":"string","mode":"ideas","locationCode":2840,"languageCode":"en","filters":{},"sort":{"key":"volume","direction":"desc"},"page":1,"pageSize":100,"format":"rows","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/keyword_explorer', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","seed":"string","mode":"ideas","locationCode":2840,"languageCode":"en","filters":{},"sort":{"key":"volume","direction":"desc"},"page":1,"pageSize":100,"format":"rows","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/keyword_explorer", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "seed": "string", "mode": "ideas", "locationCode": 2840, "languageCode": "en", "filters": {}, "sort": {"key": "volume", "direction": "desc"}, "page": 1, "pageSize": 100, "format": "rows", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/keyword_explorer", strings.NewReader(`{"clientId":"string","seed":"string","mode":"ideas","locationCode":2840,"languageCode":"en","filters":{},"sort":{"key":"volume","direction":"desc"},"page":1,"pageSize":100,"format":"rows","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Who is on the first page for a keyword ## Who is on the first page for a keyword **`POST https://seolyticize.com/api/v1/tools/serp_overview`** — Who is on the first page for a keyword The current top ten organic results for a keyword, each with domain authority, referring domains, the estimated organic traffic of that page, links into the page where counted, and every feature on the results page — plus why the keyword's difficulty is what it is. Answers 'could we get onto that page, and who would we have to beat'. Read from a recent capture when one is held, bought otherwise. Each figure says where it came from (our link graph or the vendor; panel or published click curve), and anything unknown is null, never zero. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `term` | string | yes | | | `locationCode` | integer | no | | | `languageCode` | string | no | | | `refresh` | boolean | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "term": "string", "locationCode": 2840, "languageCode": "en", "refresh": false, "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/serp_overview" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","term":"string","locationCode":2840,"languageCode":"en","refresh":false,"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/serp_overview', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","term":"string","locationCode":2840,"languageCode":"en","refresh":false,"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/serp_overview", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "term": "string", "locationCode": 2840, "languageCode": "en", "refresh": False, "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/serp_overview", strings.NewReader(`{"clientId":"string","term":"string","locationCode":2840,"languageCode":"en","refresh":false,"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What ranking first for a keyword is worth ## What ranking first for a keyword is worth **`POST https://seolyticize.com/api/v1/tools/keyword_traffic_potential`** — What ranking first for a keyword is worth Traffic potential and parent topic for a keyword: the estimated monthly organic traffic of the page ranking first, summed across every keyword that page ranks for, and that page's highest-volume keyword. Answers 'how much traffic is this keyword really worth' and 'what topic should the page be about' — a keyword's own volume undersells it. Estimated through the measured click curve and labelled with its source. Null when the leader's rankings are unknown, never zero. May buy the results page and the leader's ranked keywords when the archive holds neither. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `term` | string | yes | | | `locationCode` | integer | no | | | `languageCode` | string | no | | | `refresh` | boolean | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "term": "string", "locationCode": 2840, "languageCode": "en", "refresh": false, "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/keyword_traffic_potential" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","term":"string","locationCode":2840,"languageCode":"en","refresh":false,"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/keyword_traffic_potential', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","term":"string","locationCode":2840,"languageCode":"en","refresh":false,"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/keyword_traffic_potential", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "term": "string", "locationCode": 2840, "languageCode": "en", "refresh": False, "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/keyword_traffic_potential", strings.NewReader(`{"clientId":"string","term":"string","locationCode":2840,"languageCode":"en","refresh":false,"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Queries just off the top, by page ## Queries just off the top, by page **`POST https://seolyticize.com/api/v1/tools/striking_distance`** — Queries just off the top, by page Search Console queries where the client already ranks around positions four to twenty with meaningful impressions, grouped by the page that ranks. Each has the expected click-through rate now and at the top three from the measured click curve, the extra monthly clicks a move to third (and to first) would bring, and an opportunity score that favours queries nearest the top. Answers 'which existing pages should we improve first'. Read from stored console data; buys nothing. Null when the client has no site configured. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `days` | integer | no | | | `minPosition` | number | no | | | `maxPosition` | number | no | | | `minImpressions` | integer | no | | ```json { "clientId": "string", "days": 28, "minPosition": 4, "maxPosition": 20, "minImpressions": 50 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/striking_distance" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","days":28,"minPosition":4,"maxPosition":20,"minImpressions":50}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/striking_distance', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","days":28,"minPosition":4,"maxPosition":20,"minImpressions":50}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/striking_distance", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "days": 28, "minPosition": 4, "maxPosition": 20, "minImpressions": 50}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/striking_distance", strings.NewReader(`{"clientId":"string","days":28,"minPosition":4,"maxPosition":20,"minImpressions":50}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Tag tracked keywords ## Tag tracked keywords **`POST https://seolyticize.com/api/v1/tools/tag_keywords`** — Tag tracked keywords Puts tags on a tracker's keywords, or takes them off with remove: true. Answers 'label these as our brand terms'. Tags are case-insensitive labels such as 'brand' or 'money terms' that the tracker view filters by. Keywords the tracker does not track are returned in notTracked rather than tagged. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `keywordIds` | string[] | yes | | | `tags` | string[] | yes | | | `remove` | boolean | no | | ```json { "clientId": "string", "trackerId": "string", "keywordIds": [ "string" ], "tags": [ "string" ], "remove": false } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/tag_keywords" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","keywordIds":["string"],"tags":["string"],"remove":false}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/tag_keywords', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","keywordIds":["string"],"tags":["string"],"remove":false}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/tag_keywords", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "keywordIds": ["string"], "tags": ["string"], "remove": False}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/tag_keywords", strings.NewReader(`{"clientId":"string","trackerId":"string","keywordIds":["string"],"tags":["string"],"remove":false}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get rank history ## Get rank history **`POST https://seolyticize.com/api/v1/tools/get_rank_history`** — Get rank history Day-by-day positions for a tracker's keywords, each day with the page that ranked and the search features on the page — and which of those the client held. Answers 'how has this keyword moved, and with which page'. Mobile and desktop are separate series. A null position means the page was read and the client was not on it; a missing day means nobody read the page that day. Neither is a zero. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `keywordIds` | string[] | no | | | `days` | integer | no | | | `device` | string | no | | ```json { "clientId": "string", "trackerId": "string", "keywordIds": [ "string" ], "days": 30, "device": "desktop" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_rank_history" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","keywordIds":["string"],"days":30,"device":"desktop"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_rank_history', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","keywordIds":["string"],"days":30,"device":"desktop"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_rank_history", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "keywordIds": ["string"], "days": 30, "device": "desktop"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_rank_history", strings.NewReader(`{"clientId":"string","trackerId":"string","keywordIds":["string"],"days":30,"device":"desktop"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get SERP features ## Get SERP features **`POST https://seolyticize.com/api/v1/tools/get_serp_features`** — Get SERP features Which search features — featured snippet, AI Overview, local pack, video, People Also Ask and others — appeared on each tracked keyword's latest page, and which the client holds, with totals per feature. Answers 'which snippets and overviews do we own'. Features are null for a capture whose source did not report them, which is not the same as a page with none. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `device` | string | no | | ```json { "clientId": "string", "trackerId": "string", "device": "desktop" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_serp_features" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","device":"desktop"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_serp_features', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","device":"desktop"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_serp_features", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "device": "desktop"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_serp_features", strings.NewReader(`{"clientId":"string","trackerId":"string","device":"desktop"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get position distribution ## Get position distribution **`POST https://seolyticize.com/api/v1/tools/get_position_distribution`** — Get position distribution How a tracker's keywords spread across positions 1-3, 4-10, 11-20, 21-50 and 51-100 each day, with average position and visibility, per device. Answers 'how many keywords are on page one, and is that growing'. Keywords read and not found are counted as not ranking, apart from every band; keywords not read that day are counted as not captured. The average is over ranking keywords only. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `days` | integer | no | | | `device` | string | no | | ```json { "clientId": "string", "trackerId": "string", "days": 30, "device": "desktop" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_position_distribution" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","days":30,"device":"desktop"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_position_distribution', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","days":30,"device":"desktop"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_position_distribution", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "days": 30, "device": "desktop"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_position_distribution", strings.NewReader(`{"clientId":"string","trackerId":"string","days":30,"device":"desktop"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get ranking URL swaps ## Get ranking URL swaps **`POST https://seolyticize.com/api/v1/tools/get_url_swaps`** — Get ranking URL swaps Tracked keywords whose ranking page changed over a period — which page replaced which, when, and at what positions — flagging pages that take turns, and any keyword where several of the client's pages rank at once right now. Answers 'which keywords have two of our pages fighting over them'. Use it alongside get_cannibalization, which looks at one snapshot rather than across time. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `days` | integer | no | | | `device` | string | no | | ```json { "clientId": "string", "trackerId": "string", "days": 30, "device": "desktop" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_url_swaps" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","days":30,"device":"desktop"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_url_swaps', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","days":30,"device":"desktop"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_url_swaps", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "days": 30, "device": "desktop"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_url_swaps", strings.NewReader(`{"clientId":"string","trackerId":"string","days":30,"device":"desktop"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What Google Search Console measured ## What Google Search Console measured **`POST https://seolyticize.com/api/v1/tools/get_search_console_performance`** — What Google Search Console measured Clicks, impressions and position from the client's own Google Search Console, for one search type (web, image, video, news, discover, googleNews) over a date range, optionally filtered to one country (ISO 3166-1 alpha-3, e.g. gbr) and one device. Ask for any of the dimensions date, query, page, country and device; asking for both page and query returns the page-by-query drill-down, which has no country or device split. Answers 'what did this site actually earn in Google'. Discover and Google News have no queries and no position. Days Google is still counting are marked provisional. Returns a refusal with a reason when Search Console is not connected or a filter does not apply. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `searchType` | string | no | | | `from` | string | yes | | | `to` | string | yes | | | `country` | string | no | | | `device` | string | no | | | `dimensions` | string[] | no | | | `page` | string | no | | | `query` | string | no | | ```json { "clientId": "string", "searchType": "web", "from": "string", "to": "string", "country": null, "device": null, "dimensions": [ "date", "query", "page" ], "page": null, "query": null } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_search_console_performance" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","searchType":"web","from":"string","to":"string","country":null,"device":null,"dimensions":["date","query","page"],"page":null,"query":null}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_search_console_performance', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","searchType":"web","from":"string","to":"string","country":null,"device":null,"dimensions":["date","query","page"],"page":null,"query":null}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_search_console_performance", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "searchType": "web", "from": "string", "to": "string", "country": None, "device": None, "dimensions": ["date", "query", "page"], "page": None, "query": None}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_search_console_performance", strings.NewReader(`{"clientId":"string","searchType":"web","from":"string","to":"string","country":null,"device":null,"dimensions":["date","query","page"],"page":null,"query":null}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Ask Google what it holds for one page ## Ask Google what it holds for one page **`POST https://seolyticize.com/api/v1/tools/inspect_url`** — Ask Google what it holds for one page Runs Google's URL Inspection on one address of the client's own site: whether it is indexed, Google's coverage state, when it was last crawled, Google's chosen canonical against the site's own, mobile usability (deprecated by Google and often absent) and rich results detected. Answers 'is this page in Google, and if not why'. Spends one of the property's 2,000 daily inspections; refuses addresses that are not on a site the agency manages for this client, and says so when the day's allowance is used. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `url` | string | yes | | ```json { "clientId": "string", "url": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/inspect_url" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","url":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/inspect_url', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","url":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/inspect_url", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "url": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/inspect_url", strings.NewReader(`{"clientId":"string","url":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Which of the client's pages Google has indexed ## Which of the client's pages Google has indexed **`POST https://seolyticize.com/api/v1/tools/get_index_coverage`** — Which of the client's pages Google has indexed The newest URL Inspection verdict for every page of the client's site that has been inspected, grouped by verdict and by Google's coverage state, with pages whose Google-chosen canonical differs from their own. Answers 'how much of this site is in Google, and what is keeping the rest out'. Covers inspected pages only — Search Console's own indexing report has no API — and says how many that is and how many are still queued. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_index_coverage" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_index_coverage', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_index_coverage", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_index_coverage", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # The sitemaps each search engine knows about ## The sitemaps each search engine knows about **`POST https://seolyticize.com/api/v1/tools/list_sitemaps`** — The sitemaps each search engine knows about The client's sitemaps as Google, Bing and Yandex each report them: submitted URL counts, errors and warnings, when each was last read. Answers 'are our sitemaps being read, and are they broken'. Google no longer reports how many of a sitemap's URLs are indexed, so that figure is always null; instead, of the pages inspected, how many came from each sitemap and passed. Each engine's part is separately present, refused or failed. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_sitemaps" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_sitemaps', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_sitemaps", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_sitemaps", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Submit a sitemap to Google ## Submit a sitemap to Google **`POST https://seolyticize.com/api/v1/tools/submit_sitemap`** — Submit a sitemap to Google Submits a sitemap address to the client's Google Search Console property. Answers 'tell Google about this sitemap'. Refuses an address that is not on a site the agency manages for this client, and refuses until the client has separately allowed sitemap management (a second Google permission, never assumed from the read-only one). ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `sitemapUrl` | string | yes | | ```json { "clientId": "string", "sitemapUrl": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/submit_sitemap" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","sitemapUrl":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/submit_sitemap', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","sitemapUrl":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/submit_sitemap", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "sitemapUrl": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/submit_sitemap", strings.NewReader(`{"clientId":"string","sitemapUrl":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Remove a sitemap from Google ## Remove a sitemap from Google **`POST https://seolyticize.com/api/v1/tools/delete_sitemap`** — Remove a sitemap from Google Removes a sitemap from the client's Google Search Console property. Answers 'stop Google reading this old sitemap'. Same refusals as submit_sitemap: only addresses on a site the agency manages, and only once sitemap management has been allowed. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `sitemapUrl` | string | yes | | ```json { "clientId": "string", "sitemapUrl": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_sitemap" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","sitemapUrl":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_sitemap', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","sitemapUrl":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_sitemap", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "sitemapUrl": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_sitemap", strings.NewReader(`{"clientId":"string","sitemapUrl":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What Bing Webmaster Tools reports ## What Bing Webmaster Tools reports **`POST https://seolyticize.com/api/v1/tools/get_bing_performance`** — What Bing Webmaster Tools reports The client's site as Bing reports it, on the agency's Webmaster Tools key: daily clicks and impressions, page and query stats, crawl stats (pages crawled, in index, errors by status), crawl issues per URL, sitemaps and feeds, and the URL submission quota. Answers 'how is this site doing in Bing'. These are Bing's numbers about Bing's searchers and must never be added to Google's. Each part is separately present or failed; connected is false when the agency has no Bing key. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_bing_performance" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_bing_performance', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_bing_performance", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_bing_performance", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What Yandex Webmaster reports ## What Yandex Webmaster reports **`POST https://seolyticize.com/api/v1/tools/get_yandex_performance`** — What Yandex Webmaster reports The client's site as Yandex Webmaster reports it: site quality index, searchable and excluded pages, problems by severity, popular search queries with impressions, clicks and position, indexing history by HTTP status, sitemaps, and the recrawl queue with its quota. Answers 'how is this site doing in Yandex'. configured is false when this deployment has no Yandex app, and connected is false until the client's site owner has granted access. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | yes | | | `to` | string | yes | | ```json { "clientId": "string", "from": "string", "to": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_yandex_performance" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_yandex_performance', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_yandex_performance", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_yandex_performance", strings.NewReader(`{"clientId":"string","from":"string","to":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Push pages to Baidu ## Push pages to Baidu **`POST https://seolyticize.com/api/v1/tools/submit_baidu_urls`** — Push pages to Baidu Pushes addresses on the client's own site to Baidu through Ziyuan's link submission, the only API Baidu offers a site owner. Answers 'tell Baidu about these pages'. Returns how many Baidu accepted and how many more it will take today — Baidu reports its allowance only in reply to a push. Refuses addresses not on the client's registered site, and refuses entirely when no Baidu token has been saved. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `urls` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "urls": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/submit_baidu_urls" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","urls":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/submit_baidu_urls', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","urls":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/submit_baidu_urls", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "urls": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/submit_baidu_urls", strings.NewReader(`{"clientId":"string","urls":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Ask Yandex to recrawl pages ## Ask Yandex to recrawl pages **`POST https://seolyticize.com/api/v1/tools/submit_yandex_recrawl`** — Ask Yandex to recrawl pages Asks Yandex Webmaster to recrawl pages of the client's verified Yandex host, one task per page. Answers 'get Yandex to look at these pages again'. Returns the task id Yandex gave each queued page; get_yandex_performance shows the queue and the day's remaining quota. Refuses addresses not on the verified host (Yandex treats www and the bare domain as different hosts). The first quota refusal stops the rest, which come back as not sent. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `urls` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "urls": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/submit_yandex_recrawl" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","urls":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/submit_yandex_recrawl', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","urls":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/submit_yandex_recrawl", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "urls": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/submit_yandex_recrawl", strings.NewReader(`{"clientId":"string","urls":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Export Search Console queries or pages as CSV ## Export Search Console queries or pages as CSV **`POST https://seolyticize.com/api/v1/tools/export_search_console_data`** — Export Search Console queries or pages as CSV Google Search Console queries or pages (dimension) for a search type, date range, and optionally one country and device, as CSV with clicks, impressions, click-through rate and average position — up to 5,000 rows. Returns filename, rows and csv. Cells a spreadsheet would run as formulas are neutralised. Answers 'give me the query data in a spreadsheet'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `searchType` | string | no | | | `from` | string | yes | | | `to` | string | yes | | | `country` | string | no | | | `device` | string | no | | | `dimension` | string | no | | ```json { "clientId": "string", "searchType": "web", "from": "string", "to": "string", "country": null, "device": null, "dimension": "query" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/export_search_console_data" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","searchType":"web","from":"string","to":"string","country":null,"device":null,"dimension":"query"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/export_search_console_data', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","searchType":"web","from":"string","to":"string","country":null,"device":null,"dimension":"query"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/export_search_console_data", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "searchType": "web", "from": "string", "to": "string", "country": None, "device": None, "dimension": "query"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/export_search_console_data", strings.NewReader(`{"clientId":"string","searchType":"web","from":"string","to":"string","country":null,"device":null,"dimension":"query"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Save the client's Baidu push token ## Save the client's Baidu push token **`POST https://seolyticize.com/api/v1/tools/save_baidu_token`** — Save the client's Baidu push token Saves the link-push token from Baidu Ziyuan (ziyuan.baidu.com, link submission page) for the client's site, so submit_baidu_urls can push pages. Answers 'connect Baidu for this client'. The site must be the client's own registered site; the token is stored encrypted and never returned. Replaces any token already saved. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `siteUrl` | string | yes | | | `token` | string | yes | | ```json { "clientId": "string", "siteUrl": "string", "token": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/save_baidu_token" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","siteUrl":"string","token":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/save_baidu_token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","siteUrl":"string","token":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/save_baidu_token", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "siteUrl": "string", "token": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/save_baidu_token", strings.NewReader(`{"clientId":"string","siteUrl":"string","token":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read the client brief ## Read the client brief **`POST https://seolyticize.com/api/v1/tools/get_client_brief`** — Read the client brief Reads what the agency knows about a client: overview, current goal, positioning, audience, writing voice, competitors, key pages, custom sections, and the research log (newest first). Answers 'what do we know about this client'. Read it before researching, writing or reporting for a client, so the work matches the business and builds on earlier findings. Fields nobody has written are null, not empty. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_client_brief" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_client_brief', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_client_brief", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_client_brief", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Update the client brief ## Update the client brief **`POST https://seolyticize.com/api/v1/tools/update_client_brief`** — Update the client brief Changes the client brief that every teammate and agent reads. Answers 'remember this about the client'. Set fields (overview, goal, positioning, audience, voice; null or empty clears one, fields left out are untouched); add entries (kind competitor, key_page, section or research, each with a title and optional body and url); edit entries by id; remove entries by id (unknown ids are ignored). Applied in that order, so removing an old competitor and adding its replacement in one call works. Record a research entry when you finish a piece of work whose conclusions the next one should start from; the 50 newest research notes are kept. Returns the brief as it now stands. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `overview` | string | no | | | `goal` | string | no | | | `positioning` | string | no | | | `audience` | string | no | | | `voice` | string | no | | | `add` | object[] | no | | | `add.kind` | string | yes | | | `add.title` | string | yes | | | `add.body` | string | no | | | `add.url` | string | no | | | `update` | object[] | no | | | `update.id` | string | yes | | | `update.title` | string | no | | | `update.body` | string | no | | | `update.url` | string | no | | | `remove` | string[] | no | | ```json { "clientId": "string", "overview": "string", "goal": "string", "positioning": "string", "audience": "string", "voice": "string", "add": [ { "kind": "competitor", "title": "string", "body": "string", "url": "string" } ], "update": [ { "id": "string", "title": "string", "body": "string", "url": "string" } ], "remove": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/update_client_brief" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","overview":"string","goal":"string","positioning":"string","audience":"string","voice":"string","add":[{"kind":"competitor","title":"string","body":"string","url":"string"}],"update":[{"id":"string","title":"string","body":"string","url":"string"}],"remove":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/update_client_brief', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","overview":"string","goal":"string","positioning":"string","audience":"string","voice":"string","add":[{"kind":"competitor","title":"string","body":"string","url":"string"}],"update":[{"id":"string","title":"string","body":"string","url":"string"}],"remove":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/update_client_brief", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "overview": "string", "goal": "string", "positioning": "string", "audience": "string", "voice": "string", "add": [{"kind": "competitor", "title": "string", "body": "string", "url": "string"}], "update": [{"id": "string", "title": "string", "body": "string", "url": "string"}], "remove": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/update_client_brief", strings.NewReader(`{"clientId":"string","overview":"string","goal":"string","positioning":"string","audience":"string","voice":"string","add":[{"kind":"competitor","title":"string","body":"string","url":"string"}],"update":[{"id":"string","title":"string","body":"string","url":"string"}],"remove":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Draft the client brief from their website ## Draft the client brief from their website **`POST https://seolyticize.com/api/v1/tools/draft_client_brief`** — Draft the client brief from their website Reads the client's home page and up to four of its about, services, product or pricing pages, and drafts the brief's overview, positioning, audience and voice, plus the key pages with what each is for. Answers 'set up this client's brief'. Nothing is saved: show the draft to the user, correct it, then save what is right with update_client_brief — alreadyWritten lists fields the brief already has, which should not be overwritten without asking. Says 'Not stated on the site.' where the pages are silent, and never names competitors. A paid Claude call charged to the client's budget (costMicros reports what it cost). ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/draft_client_brief" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/draft_client_brief', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/draft_client_brief", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/draft_client_brief", strings.NewReader(`{"clientId":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List written reports ## List written reports **`POST https://seolyticize.com/api/v1/tools/list_written_reports`** — List written reports Lists the written reports saved for a client, newest change first: id, title, a plain-text summary, who wrote and last changed it, and whether it is shared. Answers 'what reports have we written for this client'. Call it before save_written_report to find a report to replace rather than writing a duplicate. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_written_reports" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_written_reports', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_written_reports", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_written_reports", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read a written report ## Read a written report **`POST https://seolyticize.com/api/v1/tools/get_written_report`** — Read a written report Reads one written report: its Markdown, the rendered HTML, its summary, who wrote it, and its public shareUrl (null while private). Answers 'show me that report'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | yes | | ```json { "clientId": "string", "reportId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_written_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_written_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_written_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_written_report", strings.NewReader(`{"clientId":"string","reportId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Save a written report ## Save a written report **`POST https://seolyticize.com/api/v1/tools/save_written_report`** — Save a written report Saves a finished report to a client, where everyone on the account can read and print it. Answers 'save this report for the client'. Write it in Markdown (headings, lists, tables, links); raw HTML is shown as text, not run. Without reportId a new report is created; with one, that report's title and text are replaced and its share link, if any, keeps working. Name templateId when it was written to a report template. New reports are private — use set_report_sharing only when the user asks for a link. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | no | | | `title` | string | yes | | | `markdown` | string | yes | | | `templateId` | string | no | | ```json { "clientId": "string", "reportId": "string", "title": "string", "markdown": "string", "templateId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/save_written_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string","title":"string","markdown":"string","templateId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/save_written_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string","title":"string","markdown":"string","templateId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/save_written_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string", "title": "string", "markdown": "string", "templateId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/save_written_report", strings.NewReader(`{"clientId":"string","reportId":"string","title":"string","markdown":"string","templateId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Delete a written report ## Delete a written report **`POST https://seolyticize.com/api/v1/tools/delete_written_report`** — Delete a written report Permanently deletes one written report; its share link stops working at once. Answers 'remove that report'. Does not touch data reports, templates or anything else. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | yes | | ```json { "clientId": "string", "reportId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_written_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_written_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_written_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_written_report", strings.NewReader(`{"clientId":"string","reportId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Share a written report by link ## Share a written report by link **`POST https://seolyticize.com/api/v1/tools/set_report_sharing`** — Share a written report by link Turns a written report's public link on or off. Answers 'give me a link I can send the client'. Anyone holding the link can read the report without an account, so only turn it on when the user explicitly asks. Turning it on for a report already shared returns the same link; turning it off breaks the link immediately, and sharing again later makes a new one. The page carries a preview card for Slack, email and social networks. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | yes | | | `shared` | boolean | yes | | ```json { "clientId": "string", "reportId": "string", "shared": true } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/set_report_sharing" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string","shared":true}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/set_report_sharing', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string","shared":true}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/set_report_sharing", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string", "shared": True}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/set_report_sharing", strings.NewReader(`{"clientId":"string","reportId":"string","shared":true}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Is organic traffic growing (GA4) ## Is organic traffic growing (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_overview`** — Is organic traffic growing (GA4) Top-line sessions, users, engaged sessions, engagement rate, key events, transactions and revenue for organic search (channel "all" for every channel), against the previous period, plus a daily sessions and key-events line. Answers 'is organic traffic improving'. total holds the figures. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `channel` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "channel": "organic" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_overview" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_overview', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_overview", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "channel": "organic"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_overview", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Where traffic comes from (GA4) ## Where traffic comes from (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_traffic_acquisition`** — Where traffic comes from (GA4) Sessions, users, engaged sessions, key events, transactions and revenue by channel group, source/medium or campaign (groupBy), against the previous period. Answers 'which channels grew or fell'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `groupBy` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "groupBy": "channel" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_traffic_acquisition" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"groupBy":"channel"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_traffic_acquisition', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"groupBy":"channel"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_traffic_acquisition", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "groupBy": "channel"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_traffic_acquisition", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"groupBy":"channel"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What landing pages did (GA4) ## What landing pages did (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_landing_pages`** — What landing pages did (GA4) Sessions, engaged sessions, engagement rate, key events, transactions and revenue by landing page, organic search by default. Answers 'which pages search sends people to, and what they did there'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `channel` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "channel": "organic" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_landing_pages" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_landing_pages', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_landing_pages", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "channel": "organic"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_landing_pages", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Page views and engagement (GA4) ## Page views and engagement (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_page_performance`** — Page views and engagement (GA4) Views, users, total engagement time in seconds and key events by page path, organic search by default (channel "all" for every channel). Answers 'which pages hold attention'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `channel` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "channel": "organic" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_page_performance" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_page_performance', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_page_performance", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "channel": "organic"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_page_performance", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"channel":"organic"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Key events (GA4) ## Key events (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_key_events`** — Key events (GA4) Counts of the property's key events (what used to be conversions) by event name, or by the organic landing page that led to them (by "landingPage"). Answers 'what are visitors converting on'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `by` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "by": "event" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_key_events" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"event"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_key_events', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"event"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_key_events", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "by": "event"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_key_events", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"event"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Shop performance (GA4) ## Shop performance (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_ecommerce`** — Shop performance (GA4) Item views, add-to-carts, purchases and item revenue by item, or transactions and purchase revenue by organic landing page (by "landingPage"). activity is "detected" or "none" — none means the property sends no ecommerce events for the period, not that nothing sold. Answers 'what is selling, and from which pages'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `by` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "by": "item" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_ecommerce" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"item"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_ecommerce', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"item"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_ecommerce", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "by": "item"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_ecommerce", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"item"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What visitors search for on the site (GA4) ## What visitors search for on the site (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_site_search`** — What visitors search for on the site (GA4) Terms people typed into the site's own search, with searches and users. measured is false when the property records no site search (enhanced measurement off, or no search box). Answers 'what are people looking for that the site does not surface'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_site_search" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_site_search', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_site_search", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_site_search", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Who arrives (GA4) ## Who arrives (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_audience`** — Who arrives (GA4) Users, sessions, engaged sessions and key events by device, country, or new versus returning (by), organic search by default. Answers 'is mobile or desktop converting, which countries'. Rows carry key, current (metrics by name) and previous (null when compare is false, metrics null where that period had no row). Also period, previousPeriod, currency, thresholded and folded. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | | `by` | string | no | | | `channel` | string | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25, "by": "device", "channel": "organic" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_audience" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"device","channel":"organic"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_audience', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"device","channel":"organic"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_audience", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25, "by": "device", "channel": "organic"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_audience", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25,"by":"device","channel":"organic"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Is the property measuring properly (GA4) ## Is the property measuring properly (GA4) **`POST https://seolyticize.com/api/v1/tools/get_ga4_measurement_health`** — Is the property measuring properly (GA4) Checks the connected property's setup: web data streams and measurement ids, enhanced measurement per stream (scrolls, outbound clicks, site search, forms, downloads), marked key events, custom dimensions and metrics, and the last day with sessions. findings lists problems, warnings and notes in plain words; a part that could not be read is reported as unread, not as fine. Answers 'can we trust this client's Analytics'. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ga4_measurement_health" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ga4_measurement_health', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ga4_measurement_health", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ga4_measurement_health", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Pages worth an edit first (Search Console × GA4) ## Pages worth an edit first (Search Console × GA4) **`POST https://seolyticize.com/api/v1/tools/get_search_opportunities`** — Pages worth an edit first (Search Console × GA4) Joins Search Console pages ranking around positions 4–20 with what their organic visitors do in GA4 over the same window, and ranks them by projected revenue, then projected key events, then extra clicks. Each row: page, extraClicks (from the click curve at third place), its top queries, the page's organic sessions, key event rate and revenue per session (analytics null where GA4 has no row for it — unknown, not worthless), and the projections. searchConsole is false when the client has no site or console data. Answers 'which page should we improve first'. Refuses with a sentence when the client has not connected a GA4 property. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `from` | string | no | | | `to` | string | no | | | `compare` | boolean | no | | | `limit` | integer | no | | ```json { "clientId": "string", "from": "string", "to": "string", "compare": true, "limit": 25 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_search_opportunities" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","from":"string","to":"string","compare":true,"limit":25}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_search_opportunities', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","from":"string","to":"string","compare":true,"limit":25}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_search_opportunities", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "from": "string", "to": "string", "compare": True, "limit": 25}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_search_opportunities", strings.NewReader(`{"clientId":"string","from":"string","to":"string","compare":true,"limit":25}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Is web analytics installed ## Is web analytics installed **`POST https://seolyticize.com/api/v1/tools/get_web_analytics_setup`** — Is web analytics installed Whether Seolyticize's own cookieless web analytics is set up for a client, with the one-line tracking snippet to install, the hostnames allowed to report, and lastEventAt (null until the first visit is recorded). Answers 'is the tracking script installed' and 'what snippet do I add to the site'. configured is false when no snippet has been created yet. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_web_analytics_setup" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_web_analytics_setup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_web_analytics_setup", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_web_analytics_setup", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Set up web analytics ## Set up web analytics **`POST https://seolyticize.com/api/v1/tools/set_up_web_analytics`** — Set up web analytics Creates a client's cookieless web analytics, or changes which hostnames may report to it, and returns the same shape as get_web_analytics_setup: the one-line tracking snippet to add to every page, the allowed hostnames and lastEventAt. Answers 'install tracking on this site'. Without hostnames it allows the client's own domain and its www form; with hostnames, those replace the list (up to 20; a visit from any other hostname is ignored, so add staging or extra domains here). Safe to repeat: an existing setup keeps its snippet. configured is false when the client has no site domain to default to and no hostnames were given. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `hostnames` | string[] | no | | | `excludePaths` | string[] | no | Paths never recorded, enforced on the server whatever the snippet says: ["/app/*", "/verify", "/reset"]. "*" matches anything. Replaces the list; [] clears it. | | `includePaths` | string[] | no | When set, only these paths are recorded: ["/", "/blog/*"]. [] clears it. | ```json { "clientId": "string", "hostnames": [ "string" ], "excludePaths": [ "string" ], "includePaths": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/set_up_web_analytics" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","hostnames":["string"],"excludePaths":["string"],"includePaths":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/set_up_web_analytics', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","hostnames":["string"],"excludePaths":["string"],"includePaths":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/set_up_web_analytics", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "hostnames": ["string"], "excludePaths": ["string"], "includePaths": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/set_up_web_analytics", strings.NewReader(`{"clientId":"string","hostnames":["string"],"excludePaths":["string"],"includePaths":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Site traffic summary (first-party) ## Site traffic summary (first-party) **`POST https://seolyticize.com/api/v1/tools/get_web_analytics_summary`** — Site traffic summary (first-party) Visitors, visits, pageviews, views per visit, bounce rate and average visit duration from Seolyticize's own web analytics, against the same span before (previous is null when that span was not fully measured), plus a daily series, realtimeVisitors in the last 30 minutes, and notes on how figures are counted. Answers 'how much traffic did the site get' for a client without GA4. Visitors are counted per day without cookies; a figure of null means not measured, not zero. Refuses with a sentence when web analytics has not been set up for the client. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `days` | number | no | The period: the last 7, 30 or 90 days, today included. | ```json { "clientId": "string", "days": 30 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_web_analytics_summary" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","days":30}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_web_analytics_summary', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","days":30}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_web_analytics_summary", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "days": 30}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_web_analytics_summary", strings.NewReader(`{"clientId":"string","days":30}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Traffic by page, source, campaign, country or device (first-party) ## Traffic by page, source, campaign, country or device (first-party) **`POST https://seolyticize.com/api/v1/tools/get_web_analytics_breakdown`** — Traffic by page, source, campaign, country or device (first-party) Lists the top rows of one breakdown from Seolyticize's own web analytics: pages (pageviews), entry_pages and exit_pages (visits), sources and referrers (visits; key null means direct), campaigns (visits, with detail "source / medium"), countries (ISO codes; null means unknown), devices, browsers, os, or custom events (event count). Each row has key, visitors and count; counts says what count counts, and totalKeys how many rows exist before the limit. Answers 'which pages and sources drive visits'. Refuses with a sentence when web analytics has not been set up for the client. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `breakdown` | string | yes | | | `days` | number | no | The period: the last 7, 30 or 90 days, today included. | | `limit` | integer | no | | ```json { "clientId": "string", "breakdown": "pages", "days": 30, "limit": 25 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_web_analytics_breakdown" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","breakdown":"pages","days":30,"limit":25}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_web_analytics_breakdown', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","breakdown":"pages","days":30,"limit":25}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_web_analytics_breakdown", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "breakdown": "pages", "days": 30, "limit": 25}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_web_analytics_breakdown", strings.NewReader(`{"clientId":"string","breakdown":"pages","days":30,"limit":25}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # AI visibility setup ## AI visibility setup **`POST https://seolyticize.com/api/v1/tools/get_ai_visibility_setup`** — AI visibility setup Reads what is tracked for a client: the questions (with their market), the brands looked for (which one is the client's own), the schedule (cadence, which assistants, last run), and which assistants this agency can ask at all and how (its own key or its proxy pool). Answers 'what are we measuring in AI assistants'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ai_visibility_setup" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ai_visibility_setup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_setup", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_setup", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Change what AI visibility tracks ## Change what AI visibility tracks **`POST https://seolyticize.com/api/v1/tools/save_ai_visibility_setup`** — Change what AI visibility tracks Adds questions (each optionally with a two-letter market), retires questions by id (past answers keep their question), and adds brands to look for (name, domain, aliases; isClient for the client's own — there should be exactly one). Questions already tracked are skipped. Write questions the way a buyer would ask an assistant, without naming any brand; suggest_ai_prompts drafts them. Answers 'track these questions'. Free; the checks that follow are what cost. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `addPrompts` | object[] | no | | | `addPrompts.text` | string | yes | | | `addPrompts.market` | string | no | | | `retirePromptIds` | string[] | no | | | `addBrands` | object[] | no | | | `addBrands.name` | string | yes | | | `addBrands.domain` | string | yes | | | `addBrands.aliases` | string[] | no | | | `addBrands.isClient` | boolean | no | | ```json { "clientId": "string", "addPrompts": [ { "text": "string", "market": "string" } ], "retirePromptIds": [ "string" ], "addBrands": [ { "name": "string", "domain": "string", "aliases": [ "string" ], "isClient": true } ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/save_ai_visibility_setup" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","addPrompts":[{"text":"string","market":"string"}],"retirePromptIds":["string"],"addBrands":[{"name":"string","domain":"string","aliases":["string"],"isClient":true}]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/save_ai_visibility_setup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","addPrompts":[{"text":"string","market":"string"}],"retirePromptIds":["string"],"addBrands":[{"name":"string","domain":"string","aliases":["string"],"isClient":true}]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/save_ai_visibility_setup", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "addPrompts": [{"text": "string", "market": "string"}], "retirePromptIds": ["string"], "addBrands": [{"name": "string", "domain": "string", "aliases": ["string"], "isClient": True}]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/save_ai_visibility_setup", strings.NewReader(`{"clientId":"string","addPrompts":[{"text":"string","market":"string"}],"retirePromptIds":["string"],"addBrands":[{"name":"string","domain":"string","aliases":["string"],"isClient":true}]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Set how often AI visibility is checked ## Set how often AI visibility is checked **`POST https://seolyticize.com/api/v1/tools/set_ai_visibility_schedule`** — Set how often AI visibility is checked Sets a client's cadence (off, daily, weekly or monthly; weekly unless changed) and which assistants it is checked on (engines; null or empty for every one the agency has configured). Every scheduled check asks every question on every chosen assistant, so show estimate_ai_visibility_cost first. Answers 'check this daily on ChatGPT and Perplexity'. Free to set. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `cadence` | string | no | | | `engines` | string[] | no | | ```json { "clientId": "string", "cadence": "off", "engines": [ "claude" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/set_ai_visibility_schedule" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","cadence":"off","engines":["claude"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/set_ai_visibility_schedule', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","cadence":"off","engines":["claude"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/set_ai_visibility_schedule", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "cadence": "off", "engines": ["claude"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/set_ai_visibility_schedule", strings.NewReader(`{"clientId":"string","cadence":"off","engines":["claude"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What an AI visibility check costs ## What an AI visibility check costs **`POST https://seolyticize.com/api/v1/tools/estimate_ai_visibility_cost`** — What an AI visibility check costs Estimates one check and a month at the current cadence: questions × assistants answers per run, and the most the Claude answers can cost (Seolyticize meters Claude; other assistants bill the agency's own accounts at their own prices, so they are counted, not priced). Answers 'how much will tracking this cost'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/estimate_ai_visibility_cost" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/estimate_ai_visibility_cost', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/estimate_ai_visibility_cost", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/estimate_ai_visibility_cost", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Run an AI visibility check now ## Run an AI visibility check now **`POST https://seolyticize.com/api/v1/tools/run_ai_visibility_check`** — Run an AI visibility check now Asks every tracked question on the chosen assistants now, in the background; results arrive within minutes in get_ai_visibility_runs and get_ai_visibility_results. The schedule is unchanged. With maxCostMicros, the check is refused before it starts when its estimate is over the limit. Get the user's go-ahead first: it spends. Answers 'check it now'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/run_ai_visibility_check" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/run_ai_visibility_check', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/run_ai_visibility_check", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/run_ai_visibility_check", strings.NewReader(`{"clientId":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # AI visibility checks so far ## AI visibility checks so far **`POST https://seolyticize.com/api/v1/tools/get_ai_visibility_runs`** — AI visibility checks so far Lists the client's checks, newest first: when, how many answers, which assistants, and in how many the client's own brand was named (mentionRate; null when no brand is marked as the client's). Answers 'when did we last check, and how did it go'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `limit` | integer | no | | ```json { "clientId": "string", "limit": 20 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ai_visibility_runs" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","limit":20}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ai_visibility_runs', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","limit":20}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_runs", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "limit": 20}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_runs", strings.NewReader(`{"clientId":"string","limit":20}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # AI visibility results, answer by answer ## AI visibility results, answer by answer **`POST https://seolyticize.com/api/v1/tools/get_ai_visibility_results`** — AI visibility results, answer by answer One check (the latest unless capturedAt names one from get_ai_visibility_runs), as a row per question and assistant: whether the client was named, where in the answer (character offset; earlier is more prominent), whether its site was cited, and which other brands were named, in order. Answers 'which questions are we missing from'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `capturedAt` | string | no | | ```json { "clientId": "string", "capturedAt": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ai_visibility_results" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","capturedAt":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ai_visibility_results', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","capturedAt":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_results", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "capturedAt": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_results", strings.NewReader(`{"clientId":"string","capturedAt":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read one AI answer in full ## Read one AI answer in full **`POST https://seolyticize.com/api/v1/tools/get_ai_visibility_answer`** — Read one AI answer in full The full text one assistant gave to one question in one check, with the pages it cited and where each tracked brand appears in it. found is false when that answer was never captured or has aged out (answers are kept 13 months). Answers 'what exactly did ChatGPT say'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `promptId` | string | yes | | | `model` | string | yes | | | `capturedAt` | string | yes | | ```json { "clientId": "string", "promptId": "string", "model": "claude", "capturedAt": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ai_visibility_answer" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","promptId":"string","model":"claude","capturedAt":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ai_visibility_answer', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","promptId":"string","model":"claude","capturedAt":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_answer", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "promptId": "string", "model": "claude", "capturedAt": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_answer", strings.NewReader(`{"clientId":"string","promptId":"string","model":"claude","capturedAt":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Is AI visibility improving ## Is AI visibility improving **`POST https://seolyticize.com/api/v1/tools/get_ai_visibility_trend`** — Is AI visibility improving The client's own mention rate over the last N days (default 28) against the N days before, overall and per assistant, counted by answer, with how many answers each side rests on. Answers 'are we being recommended more than last month'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `days` | integer | no | | ```json { "clientId": "string", "days": 28 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_ai_visibility_trend" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","days":28}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_ai_visibility_trend', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","days":28}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_trend", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "days": 28}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_ai_visibility_trend", strings.NewReader(`{"clientId":"string","days":28}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Export AI visibility data ## Export AI visibility data **`POST https://seolyticize.com/api/v1/tools/export_ai_visibility_data`** — Export AI visibility data CSV of the last N days (default 90): include "mentions" for one row per answer and brand (named, position, cited), or "answers" for the full answer text and citations (up to 2,000 answers). Returns rows and csv. Answers 'give me the raw data for a deck'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `days` | integer | no | | | `include` | string | no | | ```json { "clientId": "string", "days": 90, "include": "mentions" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/export_ai_visibility_data" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","days":90,"include":"mentions"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/export_ai_visibility_data', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","days":90,"include":"mentions"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/export_ai_visibility_data", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "days": 90, "include": "mentions"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/export_ai_visibility_data", strings.NewReader(`{"clientId":"string","days":90,"include":"mentions"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Suggest questions to track ## Suggest questions to track **`POST https://seolyticize.com/api/v1/tools/suggest_ai_prompts`** — Suggest questions to track Drafts questions worth tracking, grouped by topic, from the client brief, its brands and what is already tracked — phrased as a buyer would ask an assistant, never naming a tracked brand, never repeating a tracked question. Optionally around one topic. Nothing is saved: show them, then save the chosen ones with save_ai_visibility_setup. For questions people actually search, use keyword_explorer with mode questions. Answers 'what should we be tracking'. A metered Claude call. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `topic` | string | no | | | `count` | integer | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "topic": "string", "count": 12, "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/suggest_ai_prompts" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","topic":"string","count":12,"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/suggest_ai_prompts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","topic":"string","count":12,"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/suggest_ai_prompts", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "topic": "string", "count": 12, "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/suggest_ai_prompts", strings.NewReader(`{"clientId":"string","topic":"string","count":12,"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Read several results pages now ## Read several results pages now **`POST https://seolyticize.com/api/v1/tools/get_live_serps`** — Read several results pages now Reads the live results page for up to 10 keywords in one market and engine (Google by default), and returns each page's top results (position, type, domain, url, title) and the search features on it. Answers 'who ranks for these right now'. Each keyword is one purchase, made in turn; one that fails is reported and the rest still return; maxCostMicros stops before the purchase that would pass it. Quote with estimate_cost. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `keywords` | string[] | yes | | | `locationCode` | integer | no | | | `languageCode` | string | no | | | `engine` | string | no | | | `device` | string | no | | | `top` | integer | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "keywords": [ "string" ], "locationCode": 2840, "languageCode": "en", "engine": "google", "device": "desktop", "top": 10, "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_live_serps" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","keywords":["string"],"locationCode":2840,"languageCode":"en","engine":"google","device":"desktop","top":10,"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_live_serps', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","keywords":["string"],"locationCode":2840,"languageCode":"en","engine":"google","device":"desktop","top":10,"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_live_serps", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "keywords": ["string"], "locationCode": 2840, "languageCode": "en", "engine": "google", "device": "desktop", "top": 10, "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_live_serps", strings.NewReader(`{"clientId":"string","keywords":["string"],"locationCode":2840,"languageCode":"en","engine":"google","device":"desktop","top":10,"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Who competes across a keyword set ## Who competes across a keyword set **`POST https://seolyticize.com/api/v1/tools/find_serp_competitors`** — Who competes across a keyword set Finds the domains that rank across a set of keywords (up to 200) in one market: visibility (share of the keywords they hold in the top ten), keyword count, average position and estimated traffic. Answers 'who are our real search competitors for these terms'. List the client's own domain in exclude. One purchase per call. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `keywords` | string[] | yes | | | `locationCode` | integer | no | | | `languageCode` | string | no | | | `limit` | integer | no | | | `exclude` | string[] | no | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "keywords": [ "string" ], "locationCode": 2840, "languageCode": "en", "limit": 15, "exclude": [], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/find_serp_competitors" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","keywords":["string"],"locationCode":2840,"languageCode":"en","limit":15,"exclude":[],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/find_serp_competitors', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","keywords":["string"],"locationCode":2840,"languageCode":"en","limit":15,"exclude":[],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/find_serp_competitors", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "keywords": ["string"], "locationCode": 2840, "languageCode": "en", "limit": 15, "exclude": [], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/find_serp_competitors", strings.NewReader(`{"clientId":"string","keywords":["string"],"locationCode":2840,"languageCode":"en","limit":15,"exclude":[],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Pin tracked keywords to the top ## Pin tracked keywords to the top **`POST https://seolyticize.com/api/v1/tools/pin_tracked_keywords`** — Pin tracked keywords to the top Pins tracked keywords (by keywordId from list_tracked_keywords or get_rank_history) to the top of a tracker's table for everyone on the account, or unpins them with pinned false. Pinned keywords are listed first, in the order they were pinned. Ids not on the tracker are ignored; changed says how many moved. Answers 'keep these at the top'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `keywordIds` | string[] | yes | | | `pinned` | boolean | no | | ```json { "clientId": "string", "trackerId": "string", "keywordIds": [ "string" ], "pinned": true } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/pin_tracked_keywords" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","keywordIds":["string"],"pinned":true}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/pin_tracked_keywords', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","keywordIds":["string"],"pinned":true}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/pin_tracked_keywords", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "keywordIds": ["string"], "pinned": True}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/pin_tracked_keywords", strings.NewReader(`{"clientId":"string","trackerId":"string","keywordIds":["string"],"pinned":true}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Queries losing clicks to their snippet ## Queries losing clicks to their snippet **`POST https://seolyticize.com/api/v1/tools/get_low_ctr_queries`** — Queries losing clicks to their snippet Search Console queries on page one (positions 1–10) with enough impressions where far fewer people click than that position normally earns (under half the click curve's rate): actual and expected click-through rate and the clicks missed, worst first. Answers 'which titles and descriptions should we rewrite'. Check get_serp_features first — an AI overview or map pack above the result also lowers clicks. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `days` | integer | no | | | `minImpressions` | integer | no | | | `limit` | integer | no | | ```json { "clientId": "string", "days": 28, "minImpressions": 200, "limit": 50 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_low_ctr_queries" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","days":28,"minImpressions":200,"limit":50}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_low_ctr_queries', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","days":28,"minImpressions":200,"limit":50}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_low_ctr_queries", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "days": 28, "minImpressions": 200, "limit": 50}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_low_ctr_queries", strings.NewReader(`{"clientId":"string","days":28,"minImpressions":200,"limit":50}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Everything worth working on, in one call ## Everything worth working on, in one call **`POST https://seolyticize.com/api/v1/tools/get_opportunities`** — Everything worth working on, in one call Gathers the four kinds of opportunity from stored data: strikingDistance (pages around positions 4–20 by extra clicks), lowCtr (page-one queries losing clicks to their snippet), contentDecay (pages losing clicks), and cannibalization (pages splitting one query). Each part is a section with data or an error — an unread part is not an empty one. Answers 'what should we work on for this client'. Use the individual tools for detail. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `limit` | integer | no | | ```json { "clientId": "string", "limit": 10 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_opportunities" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","limit":10}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_opportunities', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","limit":10}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_opportunities", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "limit": 10}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_opportunities", strings.NewReader(`{"clientId":"string","limit":10}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get a client ## Get a client **`POST https://seolyticize.com/api/v1/tools/get_client`** — Get a client One client's settings with its projects, sites and rank trackers. Answers 'what is set up for this client' — use it to find tracker, project and site ids before changing anything. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_client" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_client', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_client", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_client", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Update a client ## Update a client **`POST https://seolyticize.com/api/v1/tools/update_client`** — Update a client Renames a client or changes its monthly vendor spend cap (null removes the cap). Answers 'rename this client' or 'cap this client's spend'. The cap is enforced on every paid lookup from then on. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `name` | string | no | | | `monthlyBudgetUsd` | number | no | | ```json { "clientId": "string", "name": "string", "monthlyBudgetUsd": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/update_client" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","name":"string","monthlyBudgetUsd":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/update_client', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","name":"string","monthlyBudgetUsd":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/update_client", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "name": "string", "monthlyBudgetUsd": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/update_client", strings.NewReader(`{"clientId":"string","name":"string","monthlyBudgetUsd":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Archive a client ## Archive a client **`POST https://seolyticize.com/api/v1/tools/archive_client`** — Archive a client Retires a client: it stops being tracked, crawled and reported on, stops counting against the plan, and disappears from lists. History is kept. Answers 'we no longer work with this client'. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/archive_client" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/archive_client', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/archive_client", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/archive_client", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List projects and sites ## List projects and sites **`POST https://seolyticize.com/api/v1/tools/list_projects`** — List projects and sites A client's projects with the sites under each. Lists where crawls and trackers hang from. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_projects" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_projects', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_projects", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_projects", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a project ## Create a project **`POST https://seolyticize.com/api/v1/tools/create_project`** — Create a project Adds a project to a client, optionally with a site domain to crawl. Answers 'add a second site or market for this client'. The domain is written like example.com. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `name` | string | yes | | | `domain` | string | no | | | `locationCode` | integer | no | | | `languageCode` | string | no | | ```json { "clientId": "string", "name": "string", "domain": "string", "locationCode": 0, "languageCode": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_project" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","name":"string","domain":"string","locationCode":0,"languageCode":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_project', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","name":"string","domain":"string","locationCode":0,"languageCode":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_project", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "name": "string", "domain": "string", "locationCode": 0, "languageCode": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_project", strings.NewReader(`{"clientId":"string","name":"string","domain":"string","locationCode":0,"languageCode":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Site verification and connection status ## Site verification and connection status **`POST https://seolyticize.com/api/v1/tools/get_site_status`** — Site verification and connection status For a client's sites: whether the IndexNow key file has been published and verified, which Search Console properties are connected and syncing, and the latest crawl. Answers 'is this site set up and verified'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_site_status" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_site_status', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_site_status", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_site_status", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List rank trackers ## List rank trackers **`POST https://seolyticize.com/api/v1/tools/list_rank_trackers`** — List rank trackers A client's rank trackers with engine, device, cadence and locale. Lists the tracker ids the ranking tools take. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_rank_trackers" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_rank_trackers', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_rank_trackers", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_rank_trackers", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a rank tracker ## Create a rank tracker **`POST https://seolyticize.com/api/v1/tools/create_rank_tracker`** — Create a rank tracker Starts tracking a client's positions on one engine, device and locale. Answers 'track this client on Bing too' or 'track mobile separately'. Defaults to the client's first project and site domain. Engine and locale cannot be changed later — they are part of every keyword's identity — so a different market is a new tracker. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `name` | string | yes | | | `projectId` | string | no | | | `domain` | string | no | | | `engine` | string | no | | | `device` | string | no | | | `frequency` | string | no | | | `locationCode` | integer | no | | | `languageCode` | string | no | | ```json { "clientId": "string", "name": "string", "projectId": "string", "domain": "string", "engine": "google", "device": "desktop", "frequency": "daily", "locationCode": 0, "languageCode": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_rank_tracker" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","name":"string","projectId":"string","domain":"string","engine":"google","device":"desktop","frequency":"daily","locationCode":0,"languageCode":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_rank_tracker', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","name":"string","projectId":"string","domain":"string","engine":"google","device":"desktop","frequency":"daily","locationCode":0,"languageCode":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_rank_tracker", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "name": "string", "projectId": "string", "domain": "string", "engine": "google", "device": "desktop", "frequency": "daily", "locationCode": 0, "languageCode": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_rank_tracker", strings.NewReader(`{"clientId":"string","name":"string","projectId":"string","domain":"string","engine":"google","device":"desktop","frequency":"daily","locationCode":0,"languageCode":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Update a rank tracker ## Update a rank tracker **`POST https://seolyticize.com/api/v1/tools/update_rank_tracker`** — Update a rank tracker Renames a tracker or changes its device (desktop, mobile or both) or cadence (daily or weekly). Answers 'check these weekly instead'. Weekly costs a seventh as much. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `name` | string | no | | | `device` | string | no | | | `frequency` | string | no | | ```json { "clientId": "string", "trackerId": "string", "name": "string", "device": "desktop", "frequency": "daily" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/update_rank_tracker" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","name":"string","device":"desktop","frequency":"daily"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/update_rank_tracker', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","name":"string","device":"desktop","frequency":"daily"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/update_rank_tracker", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "name": "string", "device": "desktop", "frequency": "daily"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/update_rank_tracker", strings.NewReader(`{"clientId":"string","trackerId":"string","name":"string","device":"desktop","frequency":"daily"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Delete a rank tracker ## Delete a rank tracker **`POST https://seolyticize.com/api/v1/tools/delete_rank_tracker`** — Delete a rank tracker Stops a tracker. Positions it already captured stay in reports; no further runs are made or charged. Answers 'stop tracking this'. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | ```json { "clientId": "string", "trackerId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_rank_tracker" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_rank_tracker', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_rank_tracker", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_rank_tracker", strings.NewReader(`{"clientId":"string","trackerId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List tracked keywords ## List tracked keywords **`POST https://seolyticize.com/api/v1/tools/list_tracked_keywords`** — List tracked keywords The terms a tracker measures and the competitor domains it compares against. Lists what is being paid for on every run. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | ```json { "clientId": "string", "trackerId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_tracked_keywords" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_tracked_keywords', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_tracked_keywords", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_tracked_keywords", strings.NewReader(`{"clientId":"string","trackerId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Track keywords ## Track keywords **`POST https://seolyticize.com/api/v1/tools/add_tracked_keywords`** — Track keywords Adds terms to a tracker, ignoring ones already tracked. Answers 'start tracking these keywords'. Each added term is a paid lookup on every future run; refused when the plan's keyword allowance would be exceeded. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `terms` | string[] | yes | | ```json { "clientId": "string", "trackerId": "string", "terms": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_tracked_keywords" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","terms":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_tracked_keywords', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","terms":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_tracked_keywords", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "terms": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_tracked_keywords", strings.NewReader(`{"clientId":"string","trackerId":"string","terms":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Stop tracking keywords ## Stop tracking keywords **`POST https://seolyticize.com/api/v1/tools/remove_tracked_keywords`** — Stop tracking keywords Removes terms from a tracker. Answers 'stop tracking these keywords'. Terms that were not tracked are ignored, so it is safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `terms` | string[] | yes | | ```json { "clientId": "string", "trackerId": "string", "terms": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/remove_tracked_keywords" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","terms":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/remove_tracked_keywords', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","terms":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/remove_tracked_keywords", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "terms": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/remove_tracked_keywords", strings.NewReader(`{"clientId":"string","trackerId":"string","terms":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Start a site audit crawl ## Start a site audit crawl **`POST https://seolyticize.com/api/v1/tools/start_crawl`** — Start a site audit crawl Crawls the client's site now rather than waiting for the schedule. Answers 'audit the site again'. If a crawl is already running it is returned instead of starting a second. Poll get_crawl_status with the crawl id; read results with list_crawl_issues. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/start_crawl" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/start_crawl', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/start_crawl", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/start_crawl", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get crawl status ## Get crawl status **`POST https://seolyticize.com/api/v1/tools/get_crawl_status`** — Get crawl status Whether one crawl is running, complete or failed, with page and issue counts and the failure reason. Answers 'has the audit finished'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `crawlId` | string | yes | | ```json { "clientId": "string", "crawlId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_crawl_status" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","crawlId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_crawl_status', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","crawlId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_crawl_status", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "crawlId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_crawl_status", strings.NewReader(`{"clientId":"string","crawlId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List crawls ## List crawls **`POST https://seolyticize.com/api/v1/tools/list_crawls`** — List crawls A client's recent crawls of every status, newest first. Lists what the crawler has been doing, including crawls that failed. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `limit` | integer | no | | ```json { "clientId": "string", "limit": 20 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_crawls" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","limit":20}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_crawls', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","limit":20}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_crawls", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "limit": 20}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_crawls", strings.NewReader(`{"clientId":"string","limit":20}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List site audit issues ## List site audit issues **`POST https://seolyticize.com/api/v1/tools/list_crawl_issues`** — List site audit issues Technical issues one crawl found, by rule: its readable label, severity, category, how many pages it affects, and howToFix — concrete steps to fix it and check the fix. Lists what to fix. Use crawlId 'latest' for the newest completed crawl. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `crawlId` | string | no | | ```json { "clientId": "string", "crawlId": "latest" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_crawl_issues" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","crawlId":"latest"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_crawl_issues', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","crawlId":"latest"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_crawl_issues", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "crawlId": "latest"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_crawl_issues", strings.NewReader(`{"clientId":"string","crawlId":"latest"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List reports ## List reports **`POST https://seolyticize.com/api/v1/tools/list_reports`** — List reports A client's saved reports and their recent deliveries, including failed ones with the reason. Lists what goes out and answers 'did my report go out'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_reports" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_reports', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_reports", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_reports", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a report ## Create a report **`POST https://seolyticize.com/api/v1/tools/create_report`** — Create a report Saves a report for a client from a list of sections; each is wired to the client's own tracker and site. Answers 'build a monthly report for this client'. Sections the client has nothing for yet are left out. Send it with send_report; schedules are set in the app. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `title` | string | yes | | | `sections` | string[] | yes | | ```json { "clientId": "string", "title": "string", "sections": [ "rank-summary" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","title":"string","sections":["rank-summary"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","title":"string","sections":["rank-summary"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "title": "string", "sections": ["rank-summary"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_report", strings.NewReader(`{"clientId":"string","title":"string","sections":["rank-summary"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Send a report now ## Send a report now **`POST https://seolyticize.com/api/v1/tools/send_report`** — Send a report now Sends a saved report to its scheduled recipients now, covering the month just gone. Answers 'send the report'. Recipients come from the report's schedule and cannot be named here. Pass an idempotencyKey to make a retry land on the same send instead of emailing the client twice. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | yes | | | `idempotencyKey` | string | no | | ```json { "clientId": "string", "reportId": "string", "idempotencyKey": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/send_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string","idempotencyKey":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/send_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string","idempotencyKey":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/send_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string", "idempotencyKey": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/send_report", strings.NewReader(`{"clientId":"string","reportId":"string","idempotencyKey":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List alert rules ## List alert rules **`POST https://seolyticize.com/api/v1/tools/list_alert_rules`** — List alert rules What a client's alerts watch for, at what threshold, and whether each is paused. Lists the rules; list_recent_alerts lists what they fired. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_alert_rules" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_alert_rules', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_alert_rules", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_alert_rules", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create an alert rule ## Create an alert rule **`POST https://seolyticize.com/api/v1/tools/create_alert_rule`** — Create an alert rule Raises an alert when a client's rankings, traffic, issues or backlinks cross a threshold, or the site goes down, and delivers it by channel: slack (webhookUrl, Slack's format; the default), webhook (webhookUrl, plain JSON), email (emailRecipients, up to ten) or telegram (telegramChatId, with the agency's bot from Settings). Webhooks must be https and public. Answers 'tell me when rankings drop'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `kind` | string | yes | | | `threshold` | integer | yes | | | `cooldownHours` | integer | no | | | `webhookUrl` | string | no | | | `channel` | string | no | | | `emailRecipients` | string[] | no | | | `telegramChatId` | string | no | | ```json { "clientId": "string", "kind": "rank-drop", "threshold": 0, "cooldownHours": 0, "webhookUrl": "string", "channel": "slack", "emailRecipients": [ "string" ], "telegramChatId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_alert_rule" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","kind":"rank-drop","threshold":0,"cooldownHours":0,"webhookUrl":"string","channel":"slack","emailRecipients":["string"],"telegramChatId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_alert_rule', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","kind":"rank-drop","threshold":0,"cooldownHours":0,"webhookUrl":"string","channel":"slack","emailRecipients":["string"],"telegramChatId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_alert_rule", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "kind": "rank-drop", "threshold": 0, "cooldownHours": 0, "webhookUrl": "string", "channel": "slack", "emailRecipients": ["string"], "telegramChatId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_alert_rule", strings.NewReader(`{"clientId":"string","kind":"rank-drop","threshold":0,"cooldownHours":0,"webhookUrl":"string","channel":"slack","emailRecipients":["string"],"telegramChatId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Update an alert rule ## Update an alert rule **`POST https://seolyticize.com/api/v1/tools/update_alert_rule`** — Update an alert rule Changes a rule's threshold, cooldown or delivery (channel, webhookUrl — null removes it — emailRecipients, telegramChatId), or pauses and resumes it. Answers 'this alert is too noisy', 'send this to email instead' or 'mute this for now'. Pausing keeps the rule's history. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `ruleId` | string | yes | | | `threshold` | integer | no | | | `cooldownHours` | integer | no | | | `webhookUrl` | string | no | | | `channel` | string | no | | | `emailRecipients` | string[] | no | | | `telegramChatId` | string | no | | | `paused` | boolean | no | | ```json { "clientId": "string", "ruleId": "string", "threshold": 0, "cooldownHours": 0, "webhookUrl": "string", "channel": "slack", "emailRecipients": [ "string" ], "telegramChatId": "string", "paused": true } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/update_alert_rule" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","ruleId":"string","threshold":0,"cooldownHours":0,"webhookUrl":"string","channel":"slack","emailRecipients":["string"],"telegramChatId":"string","paused":true}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/update_alert_rule', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","ruleId":"string","threshold":0,"cooldownHours":0,"webhookUrl":"string","channel":"slack","emailRecipients":["string"],"telegramChatId":"string","paused":true}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/update_alert_rule", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "ruleId": "string", "threshold": 0, "cooldownHours": 0, "webhookUrl": "string", "channel": "slack", "emailRecipients": ["string"], "telegramChatId": "string", "paused": True}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/update_alert_rule", strings.NewReader(`{"clientId":"string","ruleId":"string","threshold":0,"cooldownHours":0,"webhookUrl":"string","channel":"slack","emailRecipients":["string"],"telegramChatId":"string","paused":true}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Delete an alert rule ## Delete an alert rule **`POST https://seolyticize.com/api/v1/tools/delete_alert_rule`** — Delete an alert rule Deletes a rule and every alert it ever fired. Answers 'remove this alert for good'. To stop it but keep its history, pause it with update_alert_rule instead. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `ruleId` | string | yes | | ```json { "clientId": "string", "ruleId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_alert_rule" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","ruleId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_alert_rule', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","ruleId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_alert_rule", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "ruleId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_alert_rule", strings.NewReader(`{"clientId":"string","ruleId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Which consoles are connected for a client ## Which consoles are connected for a client **`POST https://seolyticize.com/api/v1/tools/get_client_integrations`** — Which consoles are connected for a client Whether a client's Google Search Console and Google Analytics properties are connected and syncing, with the last sync error, and whether IndexNow is set up. Answers 'is Search Console connected for this client'. Never returns tokens. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_client_integrations" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_client_integrations', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_client_integrations", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_client_integrations", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Vendor spend for one client ## Vendor spend for one client **`POST https://seolyticize.com/api/v1/tools/get_client_costs`** — Vendor spend for one client What one client's data cost over recent days, by operation, with request counts. Answers 'what are we paying for on this account'. Amounts are US dollars. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `sinceDays` | integer | no | | ```json { "clientId": "string", "sinceDays": 30 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_client_costs" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","sinceDays":30}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_client_costs', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","sinceDays":30}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_client_costs", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "sinceDays": 30}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_client_costs", strings.NewReader(`{"clientId":"string","sinceDays":30}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List a client's business locations ## List a client's business locations **`POST https://seolyticize.com/api/v1/tools/list_local_locations`** — List a client's business locations The client's business locations for local SEO: id, name, latitude and longitude. Answers 'which locations do we track in the map pack'. Pass an id to get_local_visibility, run_local_grid_scan or sync_local_reviews. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_local_locations" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_local_locations', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_local_locations", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_local_locations", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Add a business location ## Add a business location **`POST https://seolyticize.com/api/v1/tools/add_local_location`** — Add a business location Adds a business location to a client for local SEO, at a latitude and longitude — the centre of every grid scan around it. Answers 'start tracking this branch in the map pack'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `name` | string | yes | | | `lat` | number | yes | | | `lng` | number | yes | | ```json { "clientId": "string", "name": "string", "lat": 0, "lng": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_local_location" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","name":"string","lat":0,"lng":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_local_location', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","name":"string","lat":0,"lng":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_local_location", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "name": "string", "lat": 0, "lng": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_local_location", strings.NewReader(`{"clientId":"string","name":"string","lat":0,"lng":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Map-pack visibility and reviews for a location ## Map-pack visibility and reviews for a location **`POST https://seolyticize.com/api/v1/tools/get_local_visibility`** — Map-pack visibility and reviews for a location The latest grid scan for a search term at one location — the map-pack position at every point, and the share of points ranking in the top 3 and top 10 — plus an analysis of the location's reviews (rating, volume, recency, response rate). Answers 'where in town do we show up for this search'. scanned is false when that term has never been scanned there: no grid, rather than a grid of blanks that would read as invisible everywhere. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `locationId` | string | yes | | | `term` | string | yes | | ```json { "clientId": "string", "locationId": "string", "term": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_local_visibility" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","locationId":"string","term":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_local_visibility', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","locationId":"string","term":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_local_visibility", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "locationId": "string", "term": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_local_visibility", strings.NewReader(`{"clientId":"string","locationId":"string","term":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Run a map-pack grid scan ## Run a map-pack grid scan **`POST https://seolyticize.com/api/v1/tools/run_local_grid_scan`** — Run a map-pack grid scan Searches the term at every point of a size × size grid around the location (1.5 km apart) and records where the business appears in the map pack. Every point is a paid lookup, so a 7 × 7 grid is 49 lookups: quote it with estimate_cost and get the user's go-ahead first. Runs in the background; read the result with get_local_visibility a few minutes later. With maxCostMicros, the scan is refused before it starts when its estimate is over the limit. Answers 'scan our map-pack visibility now'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `locationId` | string | yes | | | `term` | string | yes | | | `size` | number | no | Grid width in points: 3, 5 or 7. | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "locationId": "string", "term": "string", "size": 5, "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/run_local_grid_scan" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","locationId":"string","term":"string","size":5,"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/run_local_grid_scan', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","locationId":"string","term":"string","size":5,"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/run_local_grid_scan", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "locationId": "string", "term": "string", "size": 5, "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/run_local_grid_scan", strings.NewReader(`{"clientId":"string","locationId":"string","term":"string","size":5,"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Collect a location's Google reviews ## Collect a location's Google reviews **`POST https://seolyticize.com/api/v1/tools/sync_local_reviews`** — Collect a location's Google reviews Collects the location's latest Google reviews (up to 50) from the vendor. Paid: quote it with estimate_cost first. Runs in the background and takes a few minutes; read the analysis with get_local_visibility afterwards. A second request within the hour lands on the collection already running rather than paying twice. With maxCostMicros, it is refused before it starts when its estimate is over the limit. Answers 'pull in our latest reviews'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `locationId` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "locationId": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/sync_local_reviews" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","locationId":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/sync_local_reviews', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","locationId":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/sync_local_reviews", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "locationId": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/sync_local_reviews", strings.NewReader(`{"clientId":"string","locationId":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Audit a client's backlinks ## Audit a client's backlinks **`POST https://seolyticize.com/api/v1/tools/get_backlink_audit`** — Audit a client's backlinks The client's backlinks, each scored from signals an agency can check — source authority, how many sites the source links out to, and the anchor — with a verdict of healthy, review or toxic and the reasons. Also the top referring domains, the top anchors, and the links our own crawler found (page-level, ours alone). Brand terms come from the client's name and domain, so a branded anchor is not read as paid; pass moneyTerms (commercial phrases such as 'cheap flights') to flag exact-match anchors. Answers 'are any of our links a problem'. 'review' means look at it, not that it is bad. domain is null when the client has no site yet. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `moneyTerms` | string[] | no | | ```json { "clientId": "string", "moneyTerms": [] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_backlink_audit" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","moneyTerms":[]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_backlink_audit', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","moneyTerms":[]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_backlink_audit", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "moneyTerms": []}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_backlink_audit", strings.NewReader(`{"clientId":"string","moneyTerms":[]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Do the linking pages still link? ## Do the linking pages still link? **`POST https://seolyticize.com/api/v1/tools/get_link_checks`** — Do the linking pages still link? What our own crawler last saw on each page said to link to the client: live (the link is there), lost (the page no longer links) or unknown (the page could not be read), with counts. Read from the last check, never fetched now. Answers 'which links have we lost'. Run recheck_links to check again. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_link_checks" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_link_checks', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_link_checks", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_link_checks", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Re-check the pages that link to a client ## Re-check the pages that link to a client **`POST https://seolyticize.com/api/v1/tools/recheck_links`** — Re-check the pages that link to a client Sends our crawler to the pages said to link to the client to see whether each still does — slowly and politely, so it takes a while; read the result with get_link_checks. A second request within the hour lands on the check already running. No vendor charge, but it visits other people's sites on the client's behalf, so it needs client:manage. Refused when the client has no rank tracker yet. Answers 'check our links again'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/recheck_links" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/recheck_links', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/recheck_links", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/recheck_links", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # The vendor's spam scores for linking sites ## The vendor's spam scores for linking sites **`POST https://seolyticize.com/api/v1/tools/get_backlink_spam_scores`** — The vendor's spam scores for linking sites The data vendor's own spam score (0–100, null when it has none) for each domain you pass — usually the sources from get_backlink_audit. A second opinion beside ours: a link both call bad is one to act on. Paid, one purchase per call however many domains (up to 1000): quote it with estimate_cost and get the user's go-ahead first. Answers 'what does the vendor think of these links'. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `domains` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "domains": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_backlink_spam_scores" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","domains":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_backlink_spam_scores', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","domains":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_backlink_spam_scores", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "domains": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_backlink_spam_scores", strings.NewReader(`{"clientId":"string","domains":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List a client's link prospects ## List a client's link prospects **`POST https://seolyticize.com/api/v1/tools/list_link_prospects`** — List a client's link prospects The client's outreach list: sites worth asking for a link, each with its stage (identified, contacted, replied, won, rejected), why it was added, and when it last moved, most recently moved first. Answers 'where is our link outreach'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_link_prospects" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_link_prospects', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_link_prospects", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_link_prospects", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Add sites to a client's outreach list ## Add sites to a client's outreach list **`POST https://seolyticize.com/api/v1/tools/add_link_prospects`** — Add sites to a client's outreach list Adds linking-site prospects to the client's outreach list at the identified stage — typically the domains get_link_intersect finds linking to competitors but not the client, with that as the reason. A site already on the list is left at its current stage, never reset. Answers 'add these sites to our outreach'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `prospects` | object[] | yes | | | `prospects.sourceDomain` | string | yes | | | `prospects.reason` | string | no | | ```json { "clientId": "string", "prospects": [ { "sourceDomain": "string", "reason": "string" } ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_link_prospects" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","prospects":[{"sourceDomain":"string","reason":"string"}]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_link_prospects', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","prospects":[{"sourceDomain":"string","reason":"string"}]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_link_prospects", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "prospects": [{"sourceDomain": "string", "reason": "string"}]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_link_prospects", strings.NewReader(`{"clientId":"string","prospects":[{"sourceDomain":"string","reason":"string"}]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Move a link prospect to another stage ## Move a link prospect to another stage **`POST https://seolyticize.com/api/v1/tools/set_link_prospect_status`** — Move a link prospect to another stage Moves one prospect on the client's outreach list to identified, contacted, replied, won or rejected. Take the id from list_link_prospects. Answers 'we heard back from this site'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `prospectId` | string | yes | | | `status` | string | yes | | ```json { "clientId": "string", "prospectId": "string", "status": "identified" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/set_link_prospect_status" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","prospectId":"string","status":"identified"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/set_link_prospect_status', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","prospectId":"string","status":"identified"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/set_link_prospect_status", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "prospectId": "string", "status": "identified"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/set_link_prospect_status", strings.NewReader(`{"clientId":"string","prospectId":"string","status":"identified"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Analyse a server access log ## Analyse a server access log **`POST https://seolyticize.com/api/v1/tools/analyse_server_log`** — Analyse a server access log Reads a web server access log in combined format (nginx's and Apache's default) passed as text, and reports what search and AI crawlers spent their visits on: analysis.byBot (hits per crawler), topPaths, errorPaths (path, status, hits), wastedOnErrors and wastedOnRedirects (shares of crawler requests that returned no content), aiCrawlerShare, totalHits and totalBytes. Also linesRead, linesUnparsed (lines in a format it did not recognise — many means the wrong format, not a quiet site), humanHits and truncated. Up to 64 MB per call; split a bigger log and call once per part. Nothing is stored. Answers 'what is Googlebot actually crawling on our site'. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `contents` | string | yes | | ```json { "clientId": "string", "contents": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/analyse_server_log" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","contents":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/analyse_server_log', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","contents":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/analyse_server_log", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "contents": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/analyse_server_log", strings.NewReader(`{"clientId":"string","contents":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List report schedules ## List report schedules **`POST https://seolyticize.com/api/v1/tools/list_report_schedules`** — List report schedules Every schedule that emails one of a client's saved reports: which report, monthly or weekly, the recipients, whether it is paused and when it last sent. Lists who receives what and how often. send_report sends to these recipients, so a report with no schedule has nobody to send to. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_report_schedules" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_report_schedules', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_report_schedules", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_report_schedules", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Schedule a report ## Schedule a report **`POST https://seolyticize.com/api/v1/tools/schedule_report`** — Schedule a report Puts one of a client's saved reports (see list_reports, create_report) on a monthly or weekly cadence for a list of recipients. Answers 'send this report to the client every month'. The scheduled send runs on its own; send_report sends one now, to every recipient of every schedule for that report. A report can have more than one schedule. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | yes | | | `cadence` | string | no | | | `recipients` | string[] | yes | Email addresses the report goes to. At least one. | ```json { "clientId": "string", "reportId": "string", "cadence": "monthly", "recipients": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/schedule_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string","cadence":"monthly","recipients":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/schedule_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string","cadence":"monthly","recipients":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/schedule_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string", "cadence": "monthly", "recipients": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/schedule_report", strings.NewReader(`{"clientId":"string","reportId":"string","cadence":"monthly","recipients":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Change a report schedule ## Change a report schedule **`POST https://seolyticize.com/api/v1/tools/update_report_schedule`** — Change a report schedule Changes a schedule's cadence or recipients, or pauses or resumes it. Answers 'add the new marketing lead to the monthly report' and 'stop the weekly email while the client is away'. Recipients, when given, replace the whole list. A paused schedule sends nothing until resumed. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `scheduleId` | string | yes | | | `cadence` | string | no | | | `recipients` | string[] | no | Email addresses the report goes to. At least one. | | `paused` | boolean | no | | ```json { "clientId": "string", "scheduleId": "string", "cadence": "monthly", "recipients": [ "string" ], "paused": true } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/update_report_schedule" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","scheduleId":"string","cadence":"monthly","recipients":["string"],"paused":true}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/update_report_schedule', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","scheduleId":"string","cadence":"monthly","recipients":["string"],"paused":true}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/update_report_schedule", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "scheduleId": "string", "cadence": "monthly", "recipients": ["string"], "paused": True}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/update_report_schedule", strings.NewReader(`{"clientId":"string","scheduleId":"string","cadence":"monthly","recipients":["string"],"paused":true}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Delete a report schedule ## Delete a report schedule **`POST https://seolyticize.com/api/v1/tools/delete_report_schedule`** — Delete a report schedule Removes a schedule for good: its recipients stop receiving that report. Answers 'stop sending this report'. The report itself and its past sends are kept. To stop it for a while instead, pause it with update_report_schedule. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `scheduleId` | string | yes | | ```json { "clientId": "string", "scheduleId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/delete_report_schedule" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","scheduleId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/delete_report_schedule', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","scheduleId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/delete_report_schedule", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "scheduleId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/delete_report_schedule", strings.NewReader(`{"clientId":"string","scheduleId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Retire a saved report ## Retire a saved report **`POST https://seolyticize.com/api/v1/tools/archive_report`** — Retire a saved report Retires a client's saved report so it is no longer sent, by schedule or by hand. Answers 'we no longer send this report'. Archived rather than deleted: reports already sent to the client stay downloadable. Safe to repeat. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `reportId` | string | yes | | ```json { "clientId": "string", "reportId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/archive_report" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","reportId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/archive_report', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","reportId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/archive_report", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "reportId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/archive_report", strings.NewReader(`{"clientId":"string","reportId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Add competitors to a tracker ## Add competitors to a tracker **`POST https://seolyticize.com/api/v1/tools/add_competitors`** — Add competitors to a tracker Adds competitor domains to a rank tracker, so share of voice and the competitor comparisons measure them on the same keywords. Answers 'compare us with these sites'. Returns the tracker's full competitor list. Domains already there are left as they are. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `domains` | string[] | yes | Domains such as example.com. A URL is reduced to its domain. | ```json { "clientId": "string", "trackerId": "string", "domains": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_competitors" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","domains":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_competitors', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","domains":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_competitors", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "domains": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_competitors", strings.NewReader(`{"clientId":"string","trackerId":"string","domains":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Remove competitors from a tracker ## Remove competitors from a tracker **`POST https://seolyticize.com/api/v1/tools/remove_competitors`** — Remove competitors from a tracker Stops measuring the named competitor domains on a rank tracker. Answers 'we no longer compete with this site'. Returns the tracker's remaining competitors. Naming a domain that is not on the list is not an error. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `domains` | string[] | yes | Domains such as example.com. A URL is reduced to its domain. | ```json { "clientId": "string", "trackerId": "string", "domains": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/remove_competitors" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","domains":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/remove_competitors', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","domains":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/remove_competitors", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "domains": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/remove_competitors", strings.NewReader(`{"clientId":"string","trackerId":"string","domains":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get a link for a person to connect a service ## Get a link for a person to connect a service **`POST https://seolyticize.com/api/v1/tools/get_connection_link`** — Get a link for a person to connect a service The link a PERSON must open to connect one of a client's accounts — Google Search Console (search_console), Search Console sitemap management (search_console_sitemaps), Google Analytics 4 (google_analytics), Yandex Webmaster (yandex_webmaster) or a Google Business Profile listing (google_business_profile) — with what they will be asked to approve and what is connected now. Answers 'connect Search Console for this client' and 'did the connection work'. An agent cannot complete these itself: the account owner signs in to that service and grants access. Give the url and the steps to your human, then call this again to check state. The person must be signed in to Seolyticize with permission to manage the client. google_analytics needs ga4PropertyId (the numeric property id, not a G- measurement id); google_business_profile needs locationId; search_console takes an optional propertyUrl (default: the client's site, https://domain/; a Domain property is sc-domain:example.com). Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `service` | string | yes | | | `propertyUrl` | string | no | | | `ga4PropertyId` | string | no | | | `locationId` | string | no | | ```json { "clientId": "string", "service": "search_console", "propertyUrl": "string", "ga4PropertyId": "string", "locationId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_connection_link" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","service":"search_console","propertyUrl":"string","ga4PropertyId":"string","locationId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_connection_link', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","service":"search_console","propertyUrl":"string","ga4PropertyId":"string","locationId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_connection_link", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "service": "search_console", "propertyUrl": "string", "ga4PropertyId": "string", "locationId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_connection_link", strings.NewReader(`{"clientId":"string","service":"search_console","propertyUrl":"string","ga4PropertyId":"string","locationId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Connect Search Console by service account ## Connect Search Console by service account **`POST https://seolyticize.com/api/v1/tools/connect_search_console_service_account`** — Connect Search Console by service account Connects a client's Search Console property as the agency's Google service account (get_google_service_account), once the property's owner has added that address as a user. Answers 'they added our address, connect Search Console'. Access is checked with Google first: with no propertyUrl, the client's site is tried as a Domain property (sc-domain:) and then as https:// and https://www. URL-prefix properties, and the first the account can read is connected. If none can be read yet, it is refused with the exact steps for the owner. Starts the sixteen-month backfill. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `propertyUrl` | string | no | | ```json { "clientId": "string", "propertyUrl": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/connect_search_console_service_account" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","propertyUrl":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/connect_search_console_service_account', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","propertyUrl":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/connect_search_console_service_account", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "propertyUrl": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/connect_search_console_service_account", strings.NewReader(`{"clientId":"string","propertyUrl":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Connect GA4 by service account ## Connect GA4 by service account **`POST https://seolyticize.com/api/v1/tools/connect_google_analytics_service_account`** — Connect GA4 by service account Connects a client's GA4 property as the agency's Google service account, once the property's owner has added that address with the Viewer role. Answers 'they added our address in GA4, connect it'. propertyId is GA4's numeric property id (Admin → Property details), not a G- measurement id. Access is checked with Google first, and refused with the exact steps when it is not granted yet. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `propertyId` | string | yes | | ```json { "clientId": "string", "propertyId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/connect_google_analytics_service_account" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","propertyId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/connect_google_analytics_service_account', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","propertyId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/connect_google_analytics_service_account", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "propertyId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/connect_google_analytics_service_account", strings.NewReader(`{"clientId":"string","propertyId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Connect Yandex Webmaster with a token ## Connect Yandex Webmaster with a token **`POST https://seolyticize.com/api/v1/tools/connect_yandex_with_token`** — Connect Yandex Webmaster with a token Connects a client's Yandex Webmaster with an OAuth token a person made in their own Yandex app (get_connection_link with yandex_webmaster returns the steps), instead of signing in through Seolyticize. Answers 'here is the Yandex token, connect it'. Yandex is asked whose token it is and the client's site is matched against that account's verified hosts; the response says which host matched, or that none did. Works even where this deployment has no Yandex sign-in set up. The token is stored sealed and never returned. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `token` | string | yes | | ```json { "clientId": "string", "token": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/connect_yandex_with_token" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","token":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/connect_yandex_with_token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","token":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/connect_yandex_with_token", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "token": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/connect_yandex_with_token", strings.NewReader(`{"clientId":"string","token":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # List client portal links ## List client portal links **`POST https://seolyticize.com/api/v1/tools/list_portal_links`** — List client portal links The live white-label portal links for a client: label, when each was made, and when it was last opened. Lists who can currently see the client's results without an account. The addresses themselves are not shown — they exist only at the moment a link is made. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/list_portal_links" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/list_portal_links', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/list_portal_links", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/list_portal_links", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a client portal link ## Create a client portal link **`POST https://seolyticize.com/api/v1/tools/create_portal_link`** — Create a client portal link Makes a white-label link that shows one client their own dashboard under the agency's name, with no sign-in. Answers 'give the client a link to their results'. The full address is in this response once and can never be shown again; send it to the client and keep it out of logs. Anyone holding it can see that client's results until it is revoked. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `label` | string | yes | Who the link is for, so it can be told apart and revoked later | ```json { "clientId": "string", "label": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_portal_link" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","label":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_portal_link', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","label":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_portal_link", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "label": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_portal_link", strings.NewReader(`{"clientId":"string","label":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Revoke a client portal link ## Revoke a client portal link **`POST https://seolyticize.com/api/v1/tools/revoke_portal_link`** — Revoke a client portal link Stops a portal link working, immediately and for good. Answers 'that link was forwarded' or 'the engagement ended'. A link that is already revoked, or is not this client's, is refused rather than reported as done. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `linkId` | string | yes | | ```json { "clientId": "string", "linkId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/revoke_portal_link" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","linkId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/revoke_portal_link', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","linkId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/revoke_portal_link", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "linkId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/revoke_portal_link", strings.NewReader(`{"clientId":"string","linkId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Verify a deployed site and finish onboarding ## Verify a deployed site and finish onboarding **`POST https://seolyticize.com/api/v1/tools/verify_site`** — Verify a deployed site and finish onboarding Answers 'I deployed it, is everything connected'. After the onboard_site manifest is live, checks and completes everything in one call: the IndexNow key file, Google ownership (and then adds and connects the Search Console property, with its sixteen months of history), whether analytics has received its first event, that robots.txt and the sitemap answer, and starts the first crawl. Returns a checklist where every item not done carries the exact fix and the next call. Safe to call again. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/verify_site" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/verify_site', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/verify_site", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/verify_site", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # What to do next for a site ## What to do next for a site **`POST https://seolyticize.com/api/v1/tools/get_next_steps`** — What to do next for a site Answers 'what is left to do for this site, and what call does it'. The onboarding checklist with live status — done, todo, in progress, failed or blocked and why — each with the exact tool and arguments to call next, then, once the site is set up, the weekly loop's calls with their arguments. Reads only; changes nothing. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_next_steps" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_next_steps', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_next_steps", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_next_steps", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Issue a Google site verification ## Issue a Google site verification **`POST https://seolyticize.com/api/v1/tools/issue_site_verification`** — Issue a Google site verification Answers 'prove we own the site to Google without anyone opening Search Console'. Asks Google for a verification token for the agency's own Google account and returns what to deploy: a file at the site root (file), a tag (meta) or a TXT record (dns). Then call verify_site_ownership. onboard_site does this for you. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `method` | string | no | | | `siteUrl` | string | no | A URL-prefix site other than https:///, such as https://www.example.com/. Not used for dns. | ```json { "clientId": "string", "method": "file", "siteUrl": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/issue_site_verification" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","method":"file","siteUrl":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/issue_site_verification', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","method":"file","siteUrl":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/issue_site_verification", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "method": "file", "siteUrl": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/issue_site_verification", strings.NewReader(`{"clientId":"string","method":"file","siteUrl":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Verify site ownership and connect Search Console ## Verify site ownership and connect Search Console **`POST https://seolyticize.com/api/v1/tools/verify_site_ownership`** — Verify site ownership and connect Search Console Answers 'the verification file is live, connect Search Console'. Has Google check the deployed file, tag or record (with no method, each is tried), which makes the agency's Google account a verified owner; then adds the property to its Search Console and connects it to the client, history included. addOwners makes other Google accounts owners too, so the site's people keep their own access. verify_site does this for you. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `method` | string | no | | | `siteUrl` | string | no | A URL-prefix site other than https:///, such as https://www.example.com/. Not used for dns. | | `addOwners` | string[] | no | | ```json { "clientId": "string", "method": "file", "siteUrl": "string", "addOwners": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/verify_site_ownership" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","method":"file","siteUrl":"string","addOwners":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/verify_site_ownership', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","method":"file","siteUrl":"string","addOwners":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/verify_site_ownership", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "method": "file", "siteUrl": "string", "addOwners": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/verify_site_ownership", strings.NewReader(`{"clientId":"string","method":"file","siteUrl":"string","addOwners":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Create a deploy hook ## Create a deploy hook **`POST https://seolyticize.com/api/v1/tools/create_deploy_hook`** — Create a deploy hook Answers 'tell search engines whenever we deploy'. Makes the client's deploy hook: a secret URL the site's pipeline calls after every deploy (one curl, or a Netlify/Vercel/Cloudflare Pages/Coolify notification). Each call reads the sitemap, compares it with the last one, and submits the new and changed pages to IndexNow; the first call records the baseline only. Returns the URL once, with how to call it from each platform. Making it again replaces the URL. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `sitemapUrl` | string | no | Read this sitemap instead of robots.txt's Sitemap: line or /sitemap.xml. | ```json { "clientId": "string", "sitemapUrl": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/create_deploy_hook" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","sitemapUrl":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/create_deploy_hook', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","sitemapUrl":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/create_deploy_hook", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "sitemapUrl": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/create_deploy_hook", strings.NewReader(`{"clientId":"string","sitemapUrl":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get the deploy hook's status ## Get the deploy hook's status **`POST https://seolyticize.com/api/v1/tools/get_deploy_hook`** — Get the deploy hook's status Answers 'did the last deploy notify search engines'. Whether the client has a deploy hook, how many sitemap pages it knows, and what its last call did. Never the URL itself. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_deploy_hook" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_deploy_hook', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_deploy_hook", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_deploy_hook", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Revoke the deploy hook ## Revoke the deploy hook **`POST https://seolyticize.com/api/v1/tools/revoke_deploy_hook`** — Revoke the deploy hook Answers 'that deploy hook URL leaked'. Stops the client's deploy hook URL working at once. Free. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/revoke_deploy_hook" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/revoke_deploy_hook', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/revoke_deploy_hook", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/revoke_deploy_hook", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Capture rankings now ## Capture rankings now **`POST https://seolyticize.com/api/v1/tools/refresh_rankings`** — Capture rankings now Captures the client's rank tracker now instead of at its next scheduled run: every tracked keyword on every device the tracker reads, each a paid lookup. Answers 'get me today's positions'. Quote it with estimate_cost first; with maxCostMicros the run is refused before it starts when its estimate is over the limit. Runs in the background: poll get_run_status with job 'rank', then read get_rankings or get_rank_summary. A second request within the same minute lands on the run already going rather than paying twice. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/refresh_rankings" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/refresh_rankings', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/refresh_rankings", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/refresh_rankings", strings.NewReader(`{"clientId":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Refresh domain and backlink data now ## Refresh domain and backlink data now **`POST https://seolyticize.com/api/v1/tools/refresh_domain_data`** — Refresh domain and backlink data now Refreshes everything bought about the client's domain — overview, ranked keywords, competitors, top pages and the backlink profile — plus what up to five tracked competitors rank for and who links to them. Answers 'update the backlinks and keyword data'. The dearest thing the product does: quote it with estimate_cost first; with maxCostMicros the refresh is refused before it starts when its estimate is over the limit. Runs in the background for a few minutes: poll get_run_status with job 'domain'. A second request within the hour lands on the refresh already going. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/refresh_domain_data" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/refresh_domain_data', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/refresh_domain_data", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/refresh_domain_data", strings.NewReader(`{"clientId":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get the status of a run ## Get the status of a run **`POST https://seolyticize.com/api/v1/tools/get_run_status`** — Get the status of a run How a run started with refresh_rankings or refresh_domain_data is going: running, done or failed, and — when it failed or captured only some keywords — the reason, written for an agency to act on (a missing vendor key, an unreachable site). Answers 'did that capture work'. A run id that is not this client's, or that has aged out, reads as unknown. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `job` | string | yes | | | `runId` | string | yes | | ```json { "clientId": "string", "job": "rank", "runId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_run_status" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","job":"rank","runId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_run_status', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","job":"rank","runId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_run_status", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "job": "rank", "runId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_run_status", strings.NewReader(`{"clientId":"string","job":"rank","runId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Find recent mentions of a brand or phrase ## Find recent mentions of a brand or phrase **`POST https://seolyticize.com/api/v1/tools/get_brand_mentions`** — Find recent mentions of a brand or phrase Who has written about a brand or phrase lately — the page, its title and snippet, when it was published, and how it read (positive, negative or neutral) — one result per publishing domain, up to 50. Answers 'who is talking about us' and finds unlinked mentions worth asking for a link. A paid lookup on the client's account: quote it with estimate_cost. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | The client whose account and monthly cap pay for the lookup | | `keyword` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "keyword": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_brand_mentions" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","keyword":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_brand_mentions', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","keyword":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_brand_mentions", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "keyword": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_brand_mentions", strings.NewReader(`{"clientId":"string","keyword":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get Google Trends interest for terms ## Get Google Trends interest for terms **`POST https://seolyticize.com/api/v1/tools/get_keyword_trends`** — Get Google Trends interest for terms Interest over time from Google Trends for up to five terms in the United States, each on a 0–100 scale relative to its own peak — so it shows when people care, not how many. Answers 'is this seasonal' and 'is this growing'. Different from get_search_demand, which counts people in our own panel. A paid lookup on the client's account: quote it with estimate_cost. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | The client whose account and monthly cap pay for the lookup | | `terms` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "terms": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_keyword_trends" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","terms":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_keyword_trends', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","terms":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_keyword_trends", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "terms": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_keyword_trends", strings.NewReader(`{"clientId":"string","terms":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Estimate search traffic for domains ## Estimate search traffic for domains **`POST https://seolyticize.com/api/v1/tools/estimate_domain_traffic`** — Estimate search traffic for domains A vendor's current estimate of monthly organic search traffic and ranking keywords for up to 100 domains, bought now. Answers 'how much search traffic do these competitors get'. Modelled from rankings, not measured visits. batch_domain_metrics reads what is already stored for free; use this when those figures are missing or stale. A paid lookup on the client's account: quote it with estimate_cost. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | The client whose account and monthly cap pay for the lookup | | `targets` | string[] | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "targets": [ "string" ], "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/estimate_domain_traffic" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","targets":["string"],"maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/estimate_domain_traffic', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","targets":["string"],"maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/estimate_domain_traffic", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "targets": ["string"], "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/estimate_domain_traffic", strings.NewReader(`{"clientId":"string","targets":["string"],"maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # See which subdomains carry a site's search traffic ## See which subdomains carry a site's search traffic **`POST https://seolyticize.com/api/v1/tools/get_subdomain_traffic`** — See which subdomains carry a site's search traffic A site's subdomains ranked by estimated organic traffic and ranking keywords, up to 50. Answers 'which parts of this site carry its search presence'. get_domain_facts lists subdomain names from certificate logs for free but says nothing of their traffic. A paid lookup on the client's account: quote it with estimate_cost. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | The client whose account and monthly cap pay for the lookup | | `domain` | string | yes | | | `maxCostMicros` | integer | no | Optional spending limit for this call, in micro-USD (1,000,000 = $1). A purchase that would take the call past it is refused before anything is bought. Pass the estimate from estimate_cost, or what the user agreed to spend. | ```json { "clientId": "string", "domain": "string", "maxCostMicros": 0 } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_subdomain_traffic" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","domain":"string","maxCostMicros":0}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_subdomain_traffic', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","domain":"string","maxCostMicros":0}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_subdomain_traffic", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "domain": "string", "maxCostMicros": 0}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_subdomain_traffic", strings.NewReader(`{"clientId":"string","domain":"string","maxCostMicros":0}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Track a keyword list ## Track a keyword list **`POST https://seolyticize.com/api/v1/tools/add_list_to_tracker`** — Track a keyword list Adds every keyword on a list to one of the client's rank trackers. Answers 'start tracking everything on that list'. Keywords for another country, language or engine than the tracker reads are skipped and listed with the reason, never tracked in the wrong market. Each added keyword is a paid lookup on every future run, so quote the tracker with estimate_cost afterwards if cost matters; refused when the plan's keyword allowance would be exceeded. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `trackerId` | string | yes | | | `listId` | string | yes | | ```json { "clientId": "string", "trackerId": "string", "listId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/add_list_to_tracker" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","trackerId":"string","listId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/add_list_to_tracker', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","trackerId":"string","listId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/add_list_to_tracker", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "trackerId": "string", "listId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/add_list_to_tracker", strings.NewReader(`{"clientId":"string","trackerId":"string","listId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Get the IndexNow key ## Get the IndexNow key **`POST https://seolyticize.com/api/v1/tools/get_indexnow_key`** — Get the IndexNow key The client's IndexNow key, the file the site must serve for it, and whether that file has been verified. Lists what submit_indexnow needs in place. A null key means none has been issued yet: use issue_indexnow_key. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/get_indexnow_key" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/get_indexnow_key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/get_indexnow_key", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/get_indexnow_key", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Issue an IndexNow key ## Issue an IndexNow key **`POST https://seolyticize.com/api/v1/tools/issue_indexnow_key`** — Issue an IndexNow key Issues the client's IndexNow key and returns the file to put on the site: fileName at the site's root, containing exactly the key. Answers 'set up IndexNow'. Re-issuing replaces the key and invalidates the file already on the site, so only do it when asked to or when the key must be retired. Hand the file to whoever can publish to the site (sites running the Seolyticize WordPress plugin serve it themselves), then call verify_indexnow_key. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/issue_indexnow_key" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/issue_indexnow_key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/issue_indexnow_key", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/issue_indexnow_key", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Verify the IndexNow key file ## Verify the IndexNow key file **`POST https://seolyticize.com/api/v1/tools/verify_indexnow_key`** — Verify the IndexNow key file Fetches the key file from the client's site and records whether it is there and correct. Answers 'is IndexNow set up'. submit_indexnow submits only for a verified key. A failure says what was found instead (not served, wrong content, unreachable); a site that stops serving the file loses its verification. ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | ```json { "clientId": "string" } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/verify_indexnow_key" \ -H "Content-Type: application/json" \ -d '{"clientId":"string"}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/verify_indexnow_key', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string"}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/verify_indexnow_key", headers={ "Content-Type": "application/json", }, json={"clientId": "string"}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/verify_indexnow_key", strings.NewReader(`{"clientId":"string"}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` --- # Queue URLs for Search Console inspection ## Queue URLs for Search Console inspection **`POST https://seolyticize.com/api/v1/tools/queue_url_inspections`** — Queue URLs for Search Console inspection Queues up to 500 of the client's URLs for Search Console's URL inspection, which runs in the background within Google's daily allowance. Answers 'check whether all these pages are indexed'. URLs that are not on the client's own site are refused and listed with the reason. Read the results with get_index_coverage as they arrive; inspect_url checks one URL immediately. Needs Search Console connected (see get_connection_link). ### Request body (`application/json`) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientId` | string | yes | | | `urls` | string[] | yes | | ```json { "clientId": "string", "urls": [ "string" ] } ``` ### Responses **200** — The tool's result, under `data`. Fields: as for `200` of `POST /api/v1/tools/list_clients`. ```json { "data": {} } ``` **400** — The body was not JSON or did not match the tool's input schema. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **401** — No key, or a key that is unknown or revoked. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **403** — The key's role does not permit the tool, or the key may not reach the named client. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **404** — No tool by that name. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **422** — A well-formed request the product declined, with the reason — a duplicate name, a plan limit, nothing configured to run. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **429** — This key has used its window. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` **500** — The request could not be completed. Fields: as for `401` of `GET /api/v1/tools`. ```json { "error": "string" } ``` ### Code samples **cURL** ```curl curl -X POST "https://seolyticize.com/api/v1/tools/queue_url_inspections" \ -H "Content-Type: application/json" \ -d '{"clientId":"string","urls":["string"]}' ``` **TypeScript** ```typescript const res = await fetch('https://seolyticize.com/api/v1/tools/queue_url_inspections', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({"clientId":"string","urls":["string"]}), }); const data = await res.json(); ``` **Python** ```python import requests res = requests.request( "POST", "https://seolyticize.com/api/v1/tools/queue_url_inspections", headers={ "Content-Type": "application/json", }, json={"clientId": "string", "urls": ["string"]}, ) data = res.json() ``` **Go** ```go package main import ( "fmt" "io" "net/http" "strings" ) func main() { req, _ := http.NewRequest("POST", "https://seolyticize.com/api/v1/tools/queue_url_inspections", strings.NewReader(`{"clientId":"string","urls":["string"]}`)) req.Header.Set("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` **PHP** ```php response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ```