Skip to main content

bisibility

Part of bisibility - open-source keyword rank tracking you can self-host and automate. This repository contains the Python SDK for the Bisibility REST API.

Docs · API reference · Roadmap

Current versions are listed on PyPI.

Python SDK for the Bisibility REST API.

Install

pip install bisibility

For local development:

pip install -e .[dev]

Quickstart

import os

from bisibility import BisibilityClient

bisibility = BisibilityClient(api_key=os.environ["BISIBILITY_API_KEY"])

projects = bisibility.list_projects()
project_id = projects.data[0].id if projects.data else None

if project_id:
    created = bisibility.create_keywords(
        project_id,
        {
            "keywords": [
                {
                    "keyword": "rank tracker api",
                    "target_url": "https://example.com/rank-tracker",
                    "tags": ["api"],
                }
            ]
        },
    )

    keyword_id = created.results[0].keyword.id if created.results else None
    if keyword_id:
        check = bisibility.run_rank_check_and_wait(keyword_id)
        print(check.position, check.ranking_url)

Configuration

import os

from bisibility import BisibilityClient

bisibility = BisibilityClient(
    api_key=os.environ["BISIBILITY_API_KEY"],
    base_url="https://bisibility.com/api/v1",
    project_id=os.environ.get("BISIBILITY_PROJECT_ID"),
    timeout=30.0,
)

base_url should point at the API v1 root. Protected methods send Authorization: Bearer <api_key>. Write methods accept RequestOptions with an idempotency_key, which maps to the server Idempotency-Key header.

The client accepts project API keys (bsb_key_live_... or bsb_key_test_...) and personal access tokens (bsb_pat_live_...). For a PAT with multiple project memberships, set project_id to a project ID returned by list_projects; the client sends it as X-Bisibility-Project on project-implicit routes. PAT methods include get_me, create_project, personal-token self-management, project API-key minting, and webhook CRUD.

Legacy bsk_... and bsp_... credentials are not accepted by the v3 contract.

Async client

AsyncBisibilityClient exposes the same API operations as BisibilityClient through httpx.AsyncClient. Use async with to close an owned HTTP client, or call await client.aclose() explicitly:

import os

from bisibility import AsyncBisibilityClient


async def list_all_keywords(project_id: str) -> None:
    async with AsyncBisibilityClient(
        api_key=os.environ["BISIBILITY_API_KEY"],
        project_id=os.environ.get("BISIBILITY_PROJECT_ID"),
    ) as bisibility:
        async for keyword in bisibility.iter_keywords(project_id, {"limit": 100}):
            print(keyword.text)

create_async_bisibility_client() is the factory counterpart to create_bisibility_client(). Async requests preserve the same authentication, timeouts, retries, validation, errors, and cursor filters as the synchronous client. Retry delays are cancellable and do not block the event loop.

Public IDs and cursors v3

Every resource identifier at the HTTP boundary is a strict lowercase public ID: <prefix>_[a-z][a-z0-9]{23}. The SDK rejects raw database IDs, legacy IDs, wrong prefixes, uppercase characters, and malformed suffixes before sending a request. The same validation is applied to typed request bodies, project header, and ID-bearing API responses. A malformed success response raises BisibilityResponseError.

The registry is fixed to: al, alr, audit, check, cmp, conn, dwh, ferry, imp, inv, key, kw, mbr, ntf, pat, prj, sid, sig, svkw, tag, usr, viw, and we. Import PUBLIC_ID_PREFIXES or public_id_pattern() when another component needs the same contract.

List operations accept and return opaque v3 cursors. Pass meta.next_cursor back unchanged; the SDK rejects legacy, unversioned, and malformed cursors before a request is sent or a success response is returned.

Cloud transfer uses package version 5 only. Its project, cloud-import, and transfer-token identifiers follow the same public-ID rules.

The examples below reuse project_id and keyword_id values returned by the API, as shown in the quickstart, instead of embedding synthetic resource IDs.

The default timeout is 30 seconds per attempt. Pass timeout=None to the client to opt out globally, or RequestOptions(timeout=None) for one request. The SDK sends User-Agent: bisibility-sdk-python/<version> unless you supplied a user agent, and always sends the matching X-Bisibility-Client header. Every request also declares its origin with X-Bisibility-Source: sdk for usage reporting; a default header you pass to the client with the same name wins.

Retries

Failed requests are retried automatically up to max_retries times (default 2) when the failure is an httpx transport error or a 429/503 response. Only requests that are safe to replay are retried: idempotent GET/HEAD/PUT/DELETE requests are always eligible, while non-idempotent POST/PATCH requests are retried only when an Idempotency-Key header is present (set via RequestOptions(idempotency_key=...)). When the server sends a Retry-After header in seconds or HTTP-date form, the client waits that long (capped at 60 seconds); otherwise it uses capped exponential backoff (0.5s, 1s, 2s, ... up to 8s). Set max_retries=0 to disable retries.

bisibility = BisibilityClient(api_key="bsb_key_live_...", max_retries=3)
from bisibility import RequestOptions

bisibility.create_api_key(
    {"name": "CI"},
    request_options=RequestOptions(idempotency_key="request-123"),
)

Methods

  • Discovery: get_health, get_liveness, get_readiness, get_open_api, get_capabilities, get_llms_text
  • Public cost (no auth): get_provider_rates, get_cost_estimate
  • Projects: list_projects, get_project, update_project, delete_project, get_project_defaults, update_project_defaults. A defaults patch replaces the schedule fields (frequency, cron_expression, jitter_minutes, timezone) as a whole, while serp_depth (10, 20, 50, or 100) and serp_stop_on_match keep their stored value when omitted.
  • API keys: list_api_keys, create_api_key, revoke_api_key
  • Keywords: list_keywords, create_keywords, add_keywords, get_keyword, update_keyword, set_keyword_target_url, delete_keyword, bulk_update_keywords
  • Keyword research: research_keywords for one seed and get_keyword_metrics for batches of up to 700 keywords. Both require API write scope because a cache miss can spend provider budget. Pass estimate_only=True for a free cache-aware dry run and max_cost_cents for a best-effort request guard. A dry run answers with a cost-only KeywordResearchEstimate; every other call answers with a KeywordResearchResult. Result source diagnostics report ok, failed, or skipped, including a machine-readable reason when applicable. Both methods expose nullable provider metrics, cache metadata, and paid BYO-key cost.
  • Backlinks: analyze_backlinks and load_more_backlink_rows. Analyze with estimate_only=True for a free dry run that answers with a cost-only BacklinksEstimate; every other call answers with a BacklinksSnapshot. load_more_backlink_rows has no estimate mode and always answers with a snapshot.
  • Domain overview: analyze_domain_overview, load_domain_overview_history, load_domain_overview_keywords, and load_domain_overview_pages. Analyze with estimate_only=True before a paid request; every operation that may spend requires an explicit max_cost_cents, including 0 for a cache-only attempt. Responses preserve snake_case market, snapshot, module, provider-cost, ranked-keyword, and relevant-page fields.
  • Rank checks: list_rank_checks, run_rank_check, run_rank_check_and_wait, get_rank_check_result
  • Signals: create_signal, list_project_signals
  • Alert rules: list_alert_rules, create_alert_rule, update_alert_rule, delete_alert_rule, list_triggered_alerts, mute_triggered_alert, mark_project_alerts_read
  • Rank-history export: export_rank_history for typed cursor-paginated JSON or raw CSV text
  • Sitemap monitors: list_sitemap_monitors, update_sitemap_monitor. A project has at most one monitor, so a monitor's id is its project's id.
  • Team: list_team_members, list_team_invites, create_team_invite, revoke_project_team_invite, revoke_team_invite
  • Providers: list_providers, connect_provider, test_provider_connection, update_provider_settings, set_provider_enabled, set_provider_priority, set_primary_provider (deprecated; use set_provider_priority), disconnect_provider
  • Saved views: list_saved_views, create_saved_view, delete_project_saved_view, delete_saved_view
  • Saved keywords: list_saved_keywords, create_saved_keywords, delete_saved_keyword. create_saved_keywords accepts plain strings or SavedKeywordInput items and returns saved_count, duplicate_count, and a per-keyword results list
  • Competitors: list_competitors, add_competitor, remove_project_competitor, remove_competitor
  • Notification preferences: get_notification_preferences, update_notification_preferences
  • Migration tokens: list_migration_tokens, mint_migration_token, revoke_project_migration_token, revoke_migration_token
  • Cloud import: get_cloud_import_compatibility, import_cloud_export, create_cloud_import_session, upload_cloud_import_chunk, finalize_cloud_import_session

Cloud import v5

Cloud import accepts only version 5 packages. Every package must use strict snake_case top-level keys and include all five section arrays, even when they are empty. Package, session, chunk, and response identifiers are validated as typed public IDs before a request is sent.

from bisibility import CloudImportPackage, CloudImportSessionCreate

package = CloudImportPackage(
    version=5,
    project_id=project_id,
    keywords=[],
    alert_rules=[],
    competitors=[],
    notification_preferences=[],
    saved_views=[],
)

session = CloudImportSessionCreate(
    version=5,
    chunk_count=1,
    source_project_id=project_id,
)

The nested CloudImportKeyword, CloudImportCompetitor, CloudImportAlertRule, CloudImportNotificationPreference, and CloudImportSavedView models mirror the API schema. Alert targets are a discriminated union with type="keyword" or type="tag"; arbitrary fields and legacy aliases are rejected.

List methods return ListResponse[T] with data and meta.next_cursor. Resource methods return pydantic models matching the Bisibility API response shape.

Keyword research uses a single seed and a result limit of 100, 300, or 500. Metrics requests accept at most 700 keywords per call, matching the REST contract. Split larger batches explicitly so each response retains its own cache counts and provider cost.

Cursor-paginated resources also expose lazy iter_* generators. They preserve the supplied filters, request pages only as needed, and stop when meta.next_cursor is None:

from bisibility import ListKeywordsOptions

for keyword in bisibility.iter_keywords(
    project_id,
    ListKeywordsOptions(tag="Product", limit=100),
):
    print(keyword.text)

Iterators are available for keywords, rank checks, signals, API keys (including project API keys), webhooks, alert rules, triggered alerts, team members, team invites, providers, saved views, saved keywords, competitors, and migration tokens. The async client exposes the same iter_* names as async iterators.

Domain overview

Domain Overview reads public search-index estimates for a domain or subdomain. A cache miss can spend the project's connected provider account, so obtain the current price before explicitly accepting it:

from math import ceil

from bisibility import AnalyzeDomainOverviewOptions, DomainOverviewEstimate

estimate = bisibility.analyze_domain_overview(
    project_id,
    AnalyzeDomainOverviewOptions(
        target="example.com",
        location_code=2840,
        language_code="en",
        estimate_only=True,
    ),
)
if not isinstance(estimate.data, DomainOverviewEstimate):
    raise RuntimeError("Expected a Domain Overview estimate")

report = bisibility.analyze_domain_overview(
    project_id,
    AnalyzeDomainOverviewOptions(
        target="example.com",
        location_code=2840,
        language_code="en",
        max_cost_cents=ceil(estimate.data.estimated_cost_cents),
    ),
)

History and additional keyword/page loads use their dedicated option models and also require max_cost_cents. Cached-only callers can pass 0; the API returns a problem response instead of silently spending when the requested data is not cached.

analyze_backlinks and research_keywords can spend the project's connected provider account on a cache miss, so both take estimate_only=True for a free, cache-aware dry run. A dry run answers with a cost-only model that carries no report fields at all, so an estimate can never be mistaken for an empty profile or an empty result set. Check which one you received with isinstance:

from math import ceil

from bisibility import AnalyzeBacklinksOptions, BacklinksEstimate, BacklinksSnapshot

estimate = bisibility.analyze_backlinks(
    project_id,
    AnalyzeBacklinksOptions(target="example.com", target_scope="site", estimate_only=True),
).data
if not isinstance(estimate, BacklinksEstimate):
    raise RuntimeError("Expected a backlinks estimate")

# `cost_cents` is `0` while `cached` is true, and equals `estimated_cost_cents` otherwise.
snapshot = bisibility.analyze_backlinks(
    project_id,
    AnalyzeBacklinksOptions(
        target="example.com",
        target_scope="site",
        max_cost_cents=ceil(estimate.estimated_cost_cents),
    ),
).data
if not isinstance(snapshot, BacklinksSnapshot):
    raise RuntimeError("Expected a backlinks snapshot")
print(snapshot.summary.backlinks_total, len(snapshot.rows))

BacklinksEstimate carries target, target_scope, include_subdomains, cached, cached_until, provider, cost_cents, estimate, and estimated_cost_cents, and nothing else. BacklinksSnapshot carries the report and no estimate fields. load_more_backlink_rows always answers with a snapshot.

research_keywords works the same way, without the data envelope:

from bisibility import KeywordResearchEstimate, KeywordResearchOptions

response = bisibility.research_keywords(
    project_id,
    KeywordResearchOptions(seed="rank tracker", estimate_only=True),
)
if isinstance(response, KeywordResearchEstimate):
    for source in response.sources:
        print(source.source, source.cost_cents, source.cached)
else:
    print(response.total_count, len(response.rows))

A KeywordResearchEstimate reports estimate, cached, cost_cents, provider, connections, and one {source, cost_cents, cached} entry per planned source. Source status, returned, and reason belong to KeywordResearchResult, because a dry run fetches nothing and so has no source outcome to report.

Webhook secret rotation

To rotate a webhook secret, set hmac_secret on WebhookUpdateInput to a non-empty string. Sending a null, empty, or whitespace-only secret is rejected. Omit hmac_secret entirely to leave the existing secret unchanged; omitted fields are not included in the PATCH request.

All SDK-defined exceptions derive from BisibilityError. BisibilityApiError also exposes is_rate_limit, is_not_found, and retry_after_seconds helpers. Problem responses follow RFC 9457 and preserve extension members; sensitive response headers are removed before an API error is exposed.

There is intentionally no create_project method: the API returns 403 Forbidden for POST /projects because project-scoped API keys cannot create projects. Create projects from the Bisibility app instead.

list_keywords supports country, device, tag, topic, intent, position_gt/position_lt, search and sort filters via ListKeywordsOptions (or a plain dict); topic and intent match the keyword classification fields, case-insensitively.

Signals

Ingest deploy/CMS/API events as signals and read them back per project. create_signal requires source ("deploy", "cms", or "api") and type (shaped like category.event, e.g. "deploy.completed"); payload must serialize to 8KB or less. list_project_signals returns signals newest first and accepts source, type and a from/to time range (use the from_ field name, or the "from" key in a dict).

from bisibility import CreateSignalInput, ListSignalsOptions

emitted = bisibility.create_signal(
    CreateSignalInput(
        source="deploy",
        type="deploy.completed",
        payload={"version": "1.2.3"},
        url="https://example.com/releases/1",
    )
)

signals = bisibility.list_project_signals(
    project_id,
    ListSignalsOptions(source="deploy", from_="2026-07-01T00:00:00Z"),
)

Public cost estimates

get_provider_rates and get_cost_estimate are public discovery endpoints and work without an API key. get_cost_estimate requires keywords and accepts locations, devices (1-2), frequency ("daily", "weekly", "monthly"), provider, and a flat-rate option or plan plan selector. Both return a DataResponse whose data is typed (list[ProviderRate] and CostEstimate).

from bisibility import BisibilityClient, CostEstimateOptions

anonymous = BisibilityClient()  # no api_key needed for these calls

rates = anonymous.get_provider_rates()
estimate = anonymous.get_cost_estimate(
    CostEstimateOptions(keywords=248, frequency="daily", provider="dataforseo")
)
print(estimate.data.monthly_cost_usd)

Queued rank checks

How a requested check executes belongs to the deployment, not to the call. Where a background worker owns execution the server answers 202 Accepted with the queued run, and where checks run inline it answers 201 Created with the finished check. run_rank_check returns that union.

started = bisibility.run_rank_check(keyword_id)
if isinstance(started, RankCheckRunQueued):
    print(f"Queued as run {started.id}")

Every rank check carries the run_id of the run that produced it, which is how a queued run is followed to its result. run_rank_check_and_wait does that polling and returns the finished check, raising BisibilityTimeoutError when the deadline passes.

check = bisibility.run_rank_check_and_wait(keyword_id, timeout_seconds=120)
print(check.position, check.ranking_url)

async_mode is retained for compatibility and no longer changes what the server does.

Typed Inputs

Plain dictionaries are supported for request bodies, and pydantic input models are available when you want validation and editor completion.

from bisibility import CreateKeywordInput, CreateKeywordsBatch, KeywordScheduleInput

created = bisibility.create_keywords(
    project_id,
    CreateKeywordsBatch(
        keywords=[
            CreateKeywordInput(
                keyword="rank tracker",
                target_url="https://example.com/rank-tracker",
                tags=["api"],
                schedule=KeywordScheduleInput(
                    frequency="daily",
                    cron_expression=None,
                ),
            )
        ]
    ),
)

New app-surface endpoints also have typed request models. Plain dictionaries remain supported.

from bisibility import (
    AlertRuleInput,
    ApiKeyCreateInput,
    ConnectProviderInput,
    ProjectDefaultsPatch,
    ProviderCredentialsInput,
    UpdateProjectInput,
)

project = bisibility.update_project(project_id, UpdateProjectInput(name="Marketing site"))

defaults = bisibility.update_project_defaults(
    project_id,
    ProjectDefaultsPatch(frequency="daily", location_key="US/Texas/Austin"),
)

api_key = bisibility.create_api_key(ApiKeyCreateInput(name="CI"))

rule = bisibility.create_alert_rule(
    project_id,
    AlertRuleInput(
        channels=["email"],
        condition_type="threshold",
        name="Ranking drop",
        target_type="all",
        threshold_position=10,
    ),
)

provider = bisibility.connect_provider(
    project_id,
    "serpapi",
    ConnectProviderInput(
        credentials=ProviderCredentialsInput(api_key="serpapi-secret"),
        priority=0,
    ),
)

# `connect_provider` sends `priority` with the connect request. `0` promotes the provider
# and renumbers the fallback chain; any other value reorders it. Omit it to keep a
# reconnected provider's place and append a new one. Legacy `primary=True` maps to zero;
# `primary=False` is a no-op.
# For "plausible", `login` is the site domain configured in Plausible (its `site_id`) and
# defaults to the project domain when omitted, `api_key` is the Stats API token, and the
# optional `endpoint` points at a self-hosted instance API.
analytics = bisibility.connect_provider(
    project_id,
    "plausible",
    ConnectProviderInput(
        credentials=ProviderCredentialsInput(
            api_key="plausible-stats-api-token",
            login="example.com",
            endpoint="https://plausible.example.com/api",
        ),
    ),
)

# A successful `test_provider_connection` reports "Connected.", or
# "Connected · <detail>." for analytics providers, where the detail names the
# resolved property or site.
print(bisibility.test_provider_connection(project_id, "plausible").message)

Errors

Non-2xx API responses raise BisibilityApiError. The RFC problem details body is available on error.problem when the server returns one.

from bisibility import BisibilityApiError

try:
    bisibility.get_keyword("kw_z9y8x7w6v5u4t3s2r1q0p9n8")
except BisibilityApiError as error:
    print(error.status, error.problem.detail if error.problem else error.body)

License

Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.

Saved reports and provider budgets

Saved report reads never invoke a provider. Use fresh_until to determine freshness; state is fresh or stale, while a domain report keeps its data outcome in data_state. Own-key and credit budgets are independent. Omit a field to keep it and explicitly clear a surface to remove its budget. Credit budgets always use cents.

saved = client.list_stored_research_reports(project_id)
report = client.get_stored_research_report(project_id, "keyword_research", {"seed": "example", "result_limit": 100})
budgets = client.list_provider_budgets(project_id)
client.update_provider_budgets(project_id, "dataforseo", {"own": {"app": None}, "credits": {"programmatic": {"amount_per_month": 500, "unit": "cents"}}})

The same methods are available with await on AsyncBisibilityClient.

Release files for bisibility 0.11.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bisibility 0.11.1
File Size Uploaded
bisibility-0.11.1.tar.gz 83.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bisibility 0.11.1
File Interpreter ABI Platform
bisibility-0.11.1-py3-none-any.whl Python 3 none any Details

Total release size: 137.3 kB

Release files / bisibility-0.11.1.tar.gz

Download URL bisibility-0.11.1.tar.gz
Size 83.8 kB
Tags Source
SHA-256 checksum
How to use checksums
bc11c1b77877d0aa27ce2e0684ee06fdcb0a59b3ecb6bab91d24a8c06de4744f
BLAKE2b-256 checksum
How to use checksums
bc887576e6cf05469f1f18abc5e08aef503bd26aa819c7e04c9eab8903a732e2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / bisibility-0.11.1-py3-none-any.whl

Download URL bisibility-0.11.1-py3-none-any.whl
Size 53.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6cae1f7803829fe346deccd2e25bd9e51b8b9a0e32523238934578eec0254ea2
BLAKE2b-256 checksum
How to use checksums
7f31ddda71a24ad6b5c7aa0c0009a7802cc47b632a9dfc3ab3ce3f0a1e9a7fac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.11.1 This release

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page