supermetrics-python-sdk

API Reference

Complete reference for the supermetrics Python SDK public API.

For a narrative walkthrough of credentials, per-request overrides, response metadata, and the error taxonomy, see Authentication & Transport.

Client Classes

SupermetricsClient

Synchronous client for blocking I/O operations.

from supermetrics import SupermetricsClient

client = SupermetricsClient(
    api_key="your_api_key",
    bearer_token=None,
    token_provider=None,
    user_agent=None,
    custom_headers=None,
    timeout=30.0,
    base_url="https://api.supermetrics.com",
    dts_base_url=None,
)

Credentials: Supply exactly one of api_key, bearer_token, or token_provider. Every parameter except api_key is keyword-only.

Parameters:

Raises: SupermetricsClientError (which is also a ValueError) if zero or more than one credential is supplied, if the supplied credential is empty, if token_provider is not callable, or if an async def provider is given — use SupermetricsAsyncClient for those.

Resource Properties:

Other Properties:

Methods:

Example:

import os

# Using context manager (recommended)
with SupermetricsClient(api_key="your_key") as client:
    accounts = client.accounts.list(ds_id="GAWA")

# An OAuth bearer token instead of an API key
client = SupermetricsClient(bearer_token="otok_abc123")

# A short-lived token, re-read on every request
client = SupermetricsClient(token_provider=lambda: os.environ["SUPERMETRICS_TOKEN"])

# Manual lifecycle
client = SupermetricsClient(api_key="your_key")
try:
    accounts = client.accounts.list(ds_id="GAWA")
finally:
    client.close()

SupermetricsAsyncClient

Asynchronous client for non-blocking I/O operations.

from supermetrics import SupermetricsAsyncClient

client = SupermetricsAsyncClient(
    api_key="your_api_key",
    bearer_token=None,
    token_provider=None,
    user_agent=None,
    custom_headers=None,
    timeout=30.0,
    base_url="https://api.supermetrics.com",
    dts_base_url=None,
)

Credentials: Supply exactly one of api_key, bearer_token, or token_provider. Every parameter except api_key is keyword-only.

Parameters: Same as SupermetricsClient, with one difference: token_provider is an AsyncTokenProvider, so it may be a coroutine function (async def) or a plain callable. A coroutine provider is awaited on every request.

Raises: SupermetricsClientError (which is also a ValueError) if zero or more than one credential is supplied, if the supplied credential is empty, or if token_provider is not callable.

Resource Properties:

Other Properties:

Methods:

Example:

import asyncio
import os

from supermetrics import SupermetricsAsyncClient


async def get_valid_token() -> str:
    # Fetch or refresh a short-lived token however your application does it.
    return os.environ["SUPERMETRICS_TOKEN"]


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        accounts = await client.accounts.list(ds_id="GAWA")
        print(f"Found {len(accounts)} accounts")

    # A coroutine token provider, awaited on every request
    async with SupermetricsAsyncClient(token_provider=get_valid_token) as client:
        logins = await client.logins.list()
        print(f"Found {len(logins)} logins")


asyncio.run(main())

Per-Request Overrides

Every resource method on both clients — and every method mirrored on with_raw_response — accepts the same three keyword-only overrides. They are documented once here and are not repeated in the per-method parameter lists below.

login = client.logins.get(
    "login_abc123",
    auth_token="otok_this_caller",
    headers={"X-Span-Id": "a8f3b2c9", "Idempotency-Key": "req-42"},
    timeout=120.0,
)

Overrides are bound to context variables for the duration of the call only, and context variables are isolated per thread and per asyncio task. One shared, pooled client can therefore serve concurrent callers that each bring their own credential and tracing context. See Authentication & Transport for the full header precedence rules.

request_options()

Context manager that binds the same three overrides for a block of code, so calls inside it inherit them without passing arguments. Only arguments that are not None are bound.

from supermetrics import request_options

with request_options(auth_token="otok_abc", headers={"X-Span-Id": "s1"}, timeout=60.0):
    client.logins.list()
    client.accounts.list(ds_id="GAWA")

Parameters: auth_token, headers, timeout — all keyword-only, all optional

Raises: SupermetricsClientError if auth_token is empty or whitespace-only

Context Variables

The overrides are backed by three public ContextVar objects, which can be set directly — typically once in web-framework middleware — so that every SDK call made while handling a request inherits them. An explicit argument on a call still wins over the ambient value.

from supermetrics import current_auth_token

token = current_auth_token.set("otok_caller")
try:
    logins = client.logins.list()
finally:
    current_auth_token.reset(token)

Response Envelope

ApiResponse

ApiResponse[T] pairs the parsed result with the transport metadata of the HTTP response. It is what every method under client.with_raw_response returns; the mirrored methods keep the exact signature of their plain counterparts.

response = client.with_raw_response.logins.get("login_abc123")

The async client works the same way:

response = await async_client.with_raw_response.logins.get("login_abc123")

Attributes:

Properties:

Raises: SupermetricsClientError if the wrapped call completed without issuing any HTTP request, so there is no transport metadata to report.

Notes:

Example:

response = client.with_raw_response.accounts.list(ds_id="GAWA")

print(f"Status: {response.status_code}")
print(f"Request ID: {response.request_id}")
print(f"Accounts: {len(response.data)}")

Resources

Each resource is reachable as a property on both clients; the async client exposes the same method names on its *AsyncResource classes, which must be awaited. Every method also accepts the keyword-only auth_token, headers, and timeout overrides described in Per-Request Overrides, and is mirrored on client.with_raw_response.

The Connector Builder resources (connector_builder, connector_builder_secrets, connector_builder_logs) follow the same conventions; their individual methods are not yet covered in this reference.

LoginLinksResource

Manage data source authentication links.

create()

Create a login link for data source authentication.

link = client.login_links.create(ds_id="GAWA", description="My Analytics Connection", expiry_time=None, **kwargs)

Parameters:

Returns: LoginLink object

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

link = client.login_links.create(ds_id="GAWA", description="Analytics Connection for Q1 Report")
print(f"Authentication URL: {link.login_url}")
print(f"Link ID: {link.link_id}")

get()

Retrieve a login link by ID.

link = client.login_links.get(link_id="abc123")

Parameters:

Returns: LoginLink object with current state

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

link = client.login_links.get(link_id="abc123")
if link.login_id:
    print(f"User authenticated successfully! Login ID: {link.login_id}")
else:
    print(f"Link status: {link.status_code}")

list()

List all login links for the authenticated user.

links = client.login_links.list()

Returns: List of LoginLink objects

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

links = client.login_links.list()
for link in links:
    print(f"{link.ds_name}: {link.status_code}")

close()

Close/expire a login link.

client.login_links.close(link_id="abc123")

Parameters:

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

client.login_links.close(link_id="abc123")
print("Link closed successfully")

update()

Update a login link’s description. This is the whole of what PATCH can change.

link = client.login_links.update(link_id="abc123", description="Q4 Analytics setup")

Parameters:

Returns: LoginLink — the updated login link

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError, SupermetricsValidationError (400/422), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

update() accepts only description. A link’s data source, expiry, redirect and username requirements are fixed when it is created; the upstream PATCH body carries a single description field and nothing else. Those other properties are set at creation, through create().

Example:

link = client.login_links.update("abc123", "Q4 Analytics setup")
print(link.description)  # "Q4 Analytics setup"

LoginsResource

Retrieve login information and credentials.

get()

Retrieve a login by login ID.

login = client.logins.get(login_id="login_abc123")

Parameters:

Returns: DataSourceLogin object

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

login = client.logins.get(login_id="login_abc123")
print(f"Username: {login.username}")
print(f"Display Name: {login.display_name}")
print(f"Data Source: {login.ds_info.ds_name}")

list()

List all logins for the authenticated user.

logins = client.logins.list()

Returns: List of DataSourceLogin objects

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

logins = client.logins.list()
for login in logins:
    print(f"{login.ds_info.ds_name}: {login.username}")

get_accounts()

List the data source accounts a login can reach.

accounts = client.logins.get_accounts(login_id="login_abc123", offset=0, limit=100)

Parameters:

Returns: list[DataSourceAccount] — the accounts on this page of results

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError, SupermetricsValidationError (422), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Paginated. offset and limit are always sent as query parameters, and the total count rides in the response meta, not in the returned list. Reach it through the raw response: client.with_raw_response.logins.get_accounts(...).json_body["meta"]["paginate"]["total"].

Example:

accounts = client.logins.get_accounts("login_abc123")
for account in accounts:
    print(f"{account.account_id}: {account.name} ({account.group})")

revoke()

Revoke a login, invalidating its OAuth credentials.

revoked = client.logins.revoke(login_id="login_abc123")

Parameters:

Returns: boolTrue when the login was revoked

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError, SupermetricsValidationError (422), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

revoke() returns a bool, not None. It is a DELETE, but upstream answers HTTP 200 with {"data": {"result": true}} rather than an empty 204, and the SDK returns that result. A revoked login can no longer be used for queries or transfers; bind a fresh login through a login link to restore access.

Example:

if client.logins.revoke("login_abc123"):
    print("Login revoked")

get_by_username()

Retrieve a login by username (convenience method).

login = client.logins.get_by_username(login_username="user@example.com")

Parameters:

Returns: DataSourceLogin object

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError, ValueError (if not found)

Example:

try:
    login = client.logins.get_by_username("analytics@company.com")
    print(f"Found login: {login.login_id}")
except ValueError:
    print("Login not found")

AccountsResource

Retrieve data source accounts available for querying.

list()

List all accounts for a data source.

accounts = client.accounts.list(ds_id="GAWA", login_usernames=None, cache_minutes=None)

Parameters:

Returns: Flattened list of account objects with account_id, account_name, group_name

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

# List all GAWA accounts
accounts = client.accounts.list(ds_id="GAWA")

# Filter by specific username
accounts = client.accounts.list(ds_id="GAWA", login_usernames="analytics@company.com")

# Filter by multiple usernames
accounts = client.accounts.list(ds_id="google_ads", login_usernames=["user1@company.com", "user2@company.com"])

# Print account details
for account in accounts:
    print(f"{account.account_name} ({account.account_id})")

QueriesResource

Execute data queries to retrieve marketing data.

execute()

Execute a data query.

result = client.queries.execute(
    ds_id="GAWA",
    ds_accounts=["123456789"],
    fields=["Date", "Sessions", "Users"],
    start_date="2024-01-01",
    end_date="2024-01-07",
    **kwargs,
)

Parameters:

Returns: DataResponse object or None

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

result = client.queries.execute(
    ds_id="GAWA",
    ds_accounts=["123456789"],
    fields=["Date", "Sessions", "Users", "Pageviews"],
    start_date="2024-01-01",
    end_date="2024-01-31",
    max_rows=10000,
    filter_="Sessions > 100",
)

if result and result.data:
    print(f"Retrieved {len(result.data)} rows")
    for row in result.data:
        print(row)

get_results()

Retrieve results for a previously executed async query.

result = client.queries.get_results(query_id="query_abc123")

Parameters:

Returns: DataResponse object or None

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

# Execute query
result = client.queries.execute(
    ds_id="GAWA", ds_accounts=["123456789"], fields=["Date", "Sessions"], start_date="2024-01-01", end_date="2024-12-31"
)

# Check if async processing
if result and result.meta and result.meta.status_code == "pending":
    print(f"Query is processing... Request ID: {result.meta.request_id}")

    # Poll for results
    import time

    time.sleep(5)
    result = client.queries.get_results(query_id=result.meta.request_id)

    if result and result.meta.status_code == "success":
        print(f"Query completed! Rows: {len(result.data)}")

BackfillsResource

Schedule and manage historical data backfills for Data Warehouse transfers.

Base URL: Backfill endpoints are served by the Data Warehouse API at https://dts-api.supermetrics.com/v1, not by the core API host. Since 0.5.0 the SDK routes them there for you, so an ordinary client works:

client = SupermetricsClient(api_key="your_api_key")
client.backfills.list_incomplete(team_id=12345)   # goes to dts-api automatically

Routing is only inferred when base_url is left at its production default. If you set base_url yourself, it is taken literally and every request goes there — including the pre-0.5.0 workaround of base_url="https://dts-api.supermetrics.com/v1", which still works unchanged. To point Data Warehouse traffic somewhere specific while keeping the core API elsewhere, pass dts_base_url.

Required API key scopes:

Required user permissions:

create()

Schedule a new backfill for a transfer.

backfill = client.backfills.create(
    team_id=12345, transfer_id=456789, range_start=date(2024, 1, 1), range_end=date(2024, 1, 31)
)

Parameters:

Returns: Backfill object with status "CREATED"

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Notes:

Example:

from datetime import date
from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    backfill = client.backfills.create(
        team_id=12345, transfer_id=456789, range_start=date(2024, 1, 1), range_end=date(2024, 1, 31)
    )
    print(f"Backfill ID: {backfill.transfer_backfill_id}")
    print(f"Status: {backfill.status}")  # "CREATED"
    print(f"Total runs: {backfill.transfer_runs_total}")

get()

Retrieve a backfill by its ID.

backfill = client.backfills.get(team_id=12345, backfill_id=67890)

Parameters:

Returns: Backfill object with current status and progress

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404 if not found), NetworkError

Example:

backfill = client.backfills.get(team_id=12345, backfill_id=67890)
print(f"Status: {backfill.status}")
print(f"Progress: {backfill.transfer_runs_completed}/{backfill.transfer_runs_total}")

if backfill.error_report:
    for err in backfill.error_report:
        print(f"Error on {err.transfer_run_date}: {err.error}")

get_latest()

Retrieve the most recent backfill for a transfer.

backfill = client.backfills.get_latest(team_id=12345, transfer_id=456789)

Parameters:

Returns: Backfill object — the latest backfill regardless of status

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404 if no backfill has ever been created), NetworkError

Example:

from supermetrics import SupermetricsNotFoundError

try:
    latest = client.backfills.get_latest(team_id=12345, transfer_id=456789)
    print(f"Latest backfill: {latest.transfer_backfill_id}")
    print(f"Status: {latest.status}")
    print(f"Range: {latest.range_start_date}{latest.range_end_date}")
except SupermetricsNotFoundError:
    print("No backfill has been created for this transfer yet")

list_incomplete()

List all incomplete backfills for a team.

backfills = client.backfills.list_incomplete(team_id=12345)

Parameters:

Returns: list[Backfill] — backfills with status CREATED, SCHEDULED, RUNNING, or FAILED, sorted by creation time (newest first). Returns an empty list if none exist.

Raises: SupermetricsAuthError, SupermetricsAPIError, NetworkError

Example:

backfills = client.backfills.list_incomplete(team_id=12345)

if not backfills:
    print("No incomplete backfills")
else:
    for backfill in backfills:
        print(
            f"[{backfill.status}] Backfill {backfill.transfer_backfill_id} "
            f"(transfer {backfill.transfer_id}): "
            f"{backfill.transfer_runs_completed}/{backfill.transfer_runs_total} runs done"
        )

cancel()

Cancel a backfill by setting its status to "CANCELLED".

backfill = client.backfills.cancel(team_id=12345, backfill_id=67890)

Parameters:

Returns: Backfill object with status "CANCELLED" and updated timestamps

Raises: SupermetricsAuthError, SupermetricsValidationError (if backfill is already in a final state), SupermetricsNotFoundError (404 if not found), NetworkError

Notes:

Example:

from supermetrics import SupermetricsNotFoundError, SupermetricsValidationError

try:
    cancelled = client.backfills.cancel(team_id=12345, backfill_id=67890)
    print(f"Status: {cancelled.status}")  # "CANCELLED"
    print(f"Ended at: {cancelled.end_time}")
except SupermetricsValidationError:
    print("Cannot cancel — backfill is already in a final state")
except SupermetricsNotFoundError:
    print("Backfill not found")

Async usage (all methods above are also available on BackfillsAsyncResource):

import asyncio
from datetime import date
from supermetrics import SupermetricsAsyncClient


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        backfill = await client.backfills.create(
            team_id=12345, transfer_id=456789, range_start=date(2024, 1, 1), range_end=date(2024, 1, 31)
        )
        print(f"Created backfill: {backfill.transfer_backfill_id}")

        incomplete = await client.backfills.list_incomplete(team_id=12345)
        print(f"Incomplete backfills: {len(incomplete)}")


asyncio.run(main())

DatasourceDetailsResource

Retrieve complete configuration details for a Supermetrics data source.

get()

Fetch metadata for a data source including report types, settings, and authentication requirements.

details = client.datasource_details.get(
    team_id=12345,
    data_source_id="GAWA",
    sm_app_id=None,  # optional
)

Parameters:

Returns: DatasourceDetails object

Raises: SupermetricsAuthError, SupermetricsValidationError, SupermetricsAPIError, NetworkError

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    details = client.datasource_details.get(team_id=12345, data_source_id="GAWA")

    print(f"Name:    {details.name}")
    print(f"Status:  {details.status}")
    print(f"Premium: {details.is_premium}")

    if details.report_types:
        for rt in details.report_types:
            print(f"  Report type: {rt.id}{rt.label}")

Async usage:

import asyncio
from supermetrics import SupermetricsAsyncClient


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        details = await client.datasource_details.get(team_id=12345, data_source_id="GAWA")
        print(f"Datasource: {details.name} ({details.status})")


asyncio.run(main())

TransfersResource

Create, configure, operate, and inspect Data Warehouse transfers — the scheduled jobs that move data from a source into a warehouse destination.

Base URL: Transfer endpoints are served by the Data Warehouse API at https://dts-api.supermetrics.com/v1, not by the core API host. The SDK routes them there for you, exactly as it does for backfills. See the base-URL note under BackfillsResource for how base_url and dts_base_url interact — in particular, setting base_url yourself disables the automatic routing.

Required API key scopes: the specification documents a scope for one endpoint in this resource only — create_datasource_connection() requires dwh_transfers_write. The others inherit the Data Warehouse permissions of the credential.

Request models. create(), update(), validate(), and validate_update() take lists of TransferSchedule, TransferAccount, TransferSegment, and TransferDataSourceSetting. They are not re-exported from the top-level package; import them from the generated models package:

from supermetrics._generated.supermetrics_api_client.models import (
    TransferAccount,
    TransferDataSourceSetting,
    TransferSchedule,
    TransferSegment,
)

Response envelope. The API wraps some of these responses in {"meta": ..., "data": ...} and returns others bare. The SDK hides the difference — every method below returns the payload itself, unwrapped — but the envelope is still visible through client.with_raw_response, which is also the only way to reach the meta.request_id of a wrapped response.

list()

List the transfers belonging to a team.

transfers = client.transfers.list(team_id=12345)

Parameters:

Returns: list[TransferInfoResponse] — a summary of every non-deleted transfer owned by the team, with its state, schedule, data source, destination, and latest backfill statistics. Empty list if the team has none.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsAPIError, NetworkError

The list item is not a subset of the detail object. list() and get() return structurally different shapes for the same transfer. A TransferInfoResponse from list() identifies it as dwh_transfer_id, carries schedule as a string ("daily"), and accounts as a list of strings. The TransferConfigurationResponse from get() identifies it as transfer_id, carries schedule as a list of TransferSchedule objects, and accounts as a list of TransferAccount objects. Code written against one cannot be reused on the other; to get the full configuration of a listed transfer, call get() with its dwh_transfer_id.

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    transfers = client.transfers.list(team_id=12345)

    for transfer in transfers:
        print(f"{transfer.dwh_transfer_id}: {transfer.display_name}")
        print(f"  state={transfer.state} schedule={transfer.schedule}")
        print(f"  accounts={transfer.accounts}")  # list[str] here, objects in get()

get()

Retrieve the full configuration of a transfer.

configuration = client.transfers.get(team_id=12345, transfer_id=36091)

Parameters:

Returns: TransferConfigurationResponse — schedule, accounts, segments, data source settings, destination, and license context. Note the shape difference from list() described above.

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404 if not found), SupermetricsAPIError, NetworkError

Example:

configuration = client.transfers.get(team_id=12345, transfer_id=36091)

print(f"Name: {configuration.display_name}")
print(f"Schema: {configuration.schema_id} -> destination {configuration.destination_id}")

for entry in configuration.schedule or []:
    print(f"  {entry.run_interval} at {entry.run_hour}:00 UTC")

for account in configuration.accounts or []:
    print(f"  account {account.account_id} via login {account.login_id}")

create()

Create a new transfer. It starts running on the schedule given here.

created = client.transfers.create(
    team_id=12345,
    data_source_id="AW",
    schema_id=2,
    destination_id=8,
    display_name="AW enhanced",
    schedule=[TransferSchedule(run_interval="daily", run_hour=22, refresh_window=1)],
    accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
    segments=None,
    data_source_settings=None,
    notification_recipients=None,
    transfer_type=None,
)

Parameters:

Everything from segments onwards is keyword-only.

Returns: TransferCreatedResponse with transfer_id and transfer_name. The API answers HTTP 201 on success.

Raises: SupermetricsAuthError, SupermetricsValidationError (400 / 422), SupermetricsAPIError (including 409 on conflict), NetworkError

Notes:

Example:

from supermetrics import SupermetricsClient
from supermetrics._generated.supermetrics_api_client.models import TransferAccount, TransferSchedule

with SupermetricsClient(api_key="your_key") as client:
    created = client.transfers.create(
        team_id=12345,
        data_source_id="AW",
        schema_id=2,
        destination_id=8,
        display_name="AW enhanced",
        schedule=[TransferSchedule(run_interval="daily", run_hour=22, refresh_window=1)],
        accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
        notification_recipients=["data-team@company.com"],
    )
    print(f"Created transfer {created.transfer_id}: {created.transfer_name}")

update()

Replace the configuration of an existing transfer.

updated = client.transfers.update(
    team_id=12345,
    transfer_id=36091,
    data_source_id="AW",
    schema_id=2,
    destination_id=8,
    display_name="AW enhanced (hourly)",
    schedule=[TransferSchedule(run_interval="hourly", refresh_window=1)],
    accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
)

Parameters: team_id and transfer_id (both int, required), then exactly the fields create() takes — data_source_id, schema_id, destination_id, display_name, schedule, accounts, and the keyword-only segments, data_source_settings, notification_recipients, transfer_type.

Returns: TransferUpdatedResponse with transfer_id and transfer_name

Raises: SupermetricsAuthError, SupermetricsValidationError (400 / 422), SupermetricsNotFoundError (404), SupermetricsAPIError, NetworkError

update() replaces, it does not patch. The request schema sets additionalProperties: false and the API exposes no PATCH endpoint, so the configuration is swapped wholesale rather than merged. Every field the transfer should keep must be resent on every call — anything omitted is dropped, not retained. The safe pattern is to read the current configuration with get(), change what you need, and send all of it back. Bear in mind that get() returns a different shape than the request takes, so the fields have to be mapped rather than passed straight through.

Example:

from supermetrics._generated.supermetrics_api_client.models import TransferAccount, TransferSchedule

current = client.transfers.get(team_id=12345, transfer_id=36091)

updated = client.transfers.update(
    team_id=12345,
    transfer_id=36091,
    data_source_id=current.data_source.data_source_id,
    schema_id=current.schema_id,
    destination_id=current.destination_id,
    display_name="AW enhanced (hourly)",  # the only change
    schedule=[TransferSchedule(run_interval="hourly", refresh_window=1)],
    accounts=[TransferAccount(login_id=a.login_id, account_id=a.account_id) for a in current.accounts or []],
)
print(f"Updated {updated.transfer_id}")

delete()

Delete a transfer.

client.transfers.delete(team_id=12345, transfer_id=36091)

Parameters:

Returns: None. The API answers HTTP 204 No Content on success.

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404), SupermetricsAPIError, NetworkError

Notes:

Example:

from supermetrics import SupermetricsNotFoundError

try:
    client.transfers.delete(team_id=12345, transfer_id=36091)
    print("Transfer deleted")
except SupermetricsNotFoundError:
    print("Transfer not found")

set_state()

Pause or resume a transfer.

result = client.transfers.set_state(team_id=12345, transfer_id=36091, state="pause")

Parameters:

Returns: TransferStateUpdateResponse with result (bool) and state (str)

Raises: SupermetricsAuthError, SupermetricsValidationError (400), SupermetricsNotFoundError (404), SupermetricsAPIError, NetworkError

The verb you send and the state you get back are not the same vocabulary. The request takes the lowercase verbs "pause" and "unpause" — the SDK sends {"transfer_state": "pause"} — and those are the only two accepted values. The state field in the response is an unconstrained string that reports the transfer’s resulting state, and the value documented in the specification is the uppercase "PAUSED". There is no enum on the response side and no symmetry with the request side, so do not feed a returned state back into set_state(), and do not compare it to the verb you sent. (TransferInfoResponse.state from list() is a third free-form string, whose documented example is the lowercase "active".)

Notes:

Example:

paused = client.transfers.set_state(team_id=12345, transfer_id=36091, state="pause")
print(f"succeeded={paused.result} state={paused.state}")  # e.g. succeeded=True state=PAUSED

# ...and back again
client.transfers.set_state(team_id=12345, transfer_id=36091, state="unpause")

validate()

Validate a configuration for a new transfer without creating it.

result = client.transfers.validate(
    team_id=12345,
    data_source_id="AW",
    schema_id=2,
    destination_id=8,
    display_name="AW enhanced",
    schedule=[TransferSchedule(run_interval="daily", run_hour=22)],
    accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
)

Parameters: exactly the parameters of create() — this is its dry run.

Returns: ValidationErrorsResponse with is_valid (bool) and errors (list[ValidationError], empty when valid)

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsAPIError, NetworkError. Not SupermetricsValidationError — see below.

An invalid configuration is a result, not an exception. The API answers HTTP 200 with is_valid: false and a list of field-level errors; the specification documents no 400 and no 422 for this endpoint. validate() therefore returns that response rather than raising, which is the whole point of a dry run — branch on result.is_valid, do not wrap the call in try/except SupermetricsValidationError. The exceptions listed above are for the call itself failing (bad credential, no access, upstream fault).

A ValidationError carries only field_id and error_code — for example field_id="display_name", error_code="isEmpty". There is no human-readable message in the payload; any text shown to a user has to be produced from the code by the caller.

Example:

result = client.transfers.validate(
    team_id=12345,
    data_source_id="AW",
    schema_id=2,
    destination_id=8,
    display_name="",  # invalid on purpose
    schedule=[TransferSchedule(run_interval="daily", run_hour=22)],
    accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
)

if result.is_valid:
    print("Configuration is valid")
else:
    for error in result.errors or []:
        print(f"  {error.field_id}: {error.error_code}")  # e.g. "display_name: isEmpty"

validate_update()

Validate a configuration change against an existing transfer without applying it.

result = client.transfers.validate_update(
    team_id=12345,
    transfer_id=36091,
    data_source_id="AW",
    schema_id=2,
    destination_id=8,
    display_name="AW enhanced (hourly)",
    schedule=[TransferSchedule(run_interval="hourly")],
    accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
)

Parameters: exactly the parameters of update() — this is its dry run.

Returns: ValidationErrorsResponse

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404 if the transfer does not exist), SupermetricsAPIError, NetworkError. As with validate(), an invalid configuration is not an exception: it comes back as HTTP 200 with is_valid set to False.

Example:

result = client.transfers.validate_update(
    team_id=12345,
    transfer_id=36091,
    data_source_id="AW",
    schema_id=2,
    destination_id=8,
    display_name="AW enhanced (hourly)",
    schedule=[TransferSchedule(run_interval="hourly")],
    accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
)

if not result.is_valid:
    for error in result.errors or []:
        print(f"{error.field_id}: {error.error_code}")
else:
    client.transfers.update(...)  # same arguments, for real this time

list_available_sources()

List the data sources and destinations available to a team.

available = client.transfers.list_available_sources(team_id=12345)

Parameters:

Returns: AvailableSourcesResponse with data_sources, destinations, and destination_types

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsAPIError, NetworkError

Notes:

Example:

available = client.transfers.list_available_sources(team_id=12345)

for source in available.data_sources or []:
    print(f"{source.data_source_id}: {source.service_name}")

for destination in available.destinations or []:
    print(f"{destination.destination_id}: {destination.destination_name} ({destination.destination_type})")

get_available_options()

Get the configuration options for one source and destination combination.

options = client.transfers.get_available_options(team_id=12345, source_id="AW", destination_id=8)

Parameters:

Both source_id and destination_id are required by the API; there is no “all options” form of this call.

Returns: TransferOptionsResponse with data_source, schedule_options, schemas, logins, accounts, segments, and license_

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsAPIError, NetworkError

This response is almost entirely untyped upstream. Only data_source has described properties (data_source_id, service_name, settings). schedule_options, schemas, logins, accounts, segments, the individual data source setting items, and license_ are declared in the specification as bare objects with no properties, so the generated models carry no fields at all — every value lands in additional_properties. Read them as dictionaries, and expect the keys to be whatever the API sends rather than something the SDK can type-check.

Example:

options = client.transfers.get_available_options(team_id=12345, source_id="AW", destination_id=8)

print(f"Source: {options.data_source.data_source_id} ({options.data_source.service_name})")

# Untyped members: read through additional_properties
for schema in options.schemas or []:
    print(schema.additional_properties)

for login in options.logins or []:
    print(login.additional_properties.get("login_id"))

list_runs()

List the runs of a transfer within a date range.

runs = client.transfers.list_runs(
    team_id=12345,
    transfer_id=36091,
    start_date=datetime.datetime(2024, 1, 1),
    end_date=datetime.datetime(2024, 1, 31),
    filter_issues_only=None,
    sort_field=None,
    sort_direction=None,
    limit=None,
    offset=None,
)

Parameters:

start_date and end_date are required — there is no “all runs” form of this call — and are typed datetime.datetime. The SDK calls .isoformat() on them, so a string will not do. Everything from filter_issues_only onwards is keyword-only.

Returns: list[TransferRunItem]

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404 if the transfer does not exist), SupermetricsAPIError, NetworkError

Example:

import datetime

runs = client.transfers.list_runs(
    team_id=12345,
    transfer_id=36091,
    start_date=datetime.datetime(2024, 1, 1),
    end_date=datetime.datetime(2024, 1, 31),
    filter_issues_only=True,
    sort_field="data_date",
    sort_direction="DESC",
    limit=50,
)

for run in runs:
    print(f"{run.id} [{run.status}] {run.type_} {run.data_date}: {run.message}")

# Pass a run's id to transfer_runs.get() for the per-query detail

create_datasource_connection()

Create a data source connection for a transfer.

connection = client.transfers.create_datasource_connection(
    team_id=12345, data_source_id="ADM", destination_type="DWH_SNOWFLAKE"
)

Required API key scope: dwh_transfers_write.

Parameters:

Returns: DataSourceConnection with connection_id (a UUID) and the optional login_url / connect_url. The API answers HTTP 201 on success.

Raises: SupermetricsAuthError, SupermetricsValidationError (400 / 422), SupermetricsForbiddenError, SupermetricsAPIError, NetworkError

Notes:

Example:

connection = client.transfers.create_datasource_connection(
    team_id=12345, data_source_id="ADM", destination_type="DWH_SNOWFLAKE"
)
print(f"Connection: {connection.connection_id}")

if connection.login_url:
    print(f"Finish authentication at {connection.login_url}")

Async usage (all fourteen methods above are also available on TransfersAsyncResource):

import asyncio
import datetime

from supermetrics import SupermetricsAsyncClient
from supermetrics._generated.supermetrics_api_client.models import TransferAccount, TransferSchedule


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        transfers = await client.transfers.list(team_id=12345)
        print(f"{len(transfers)} transfers")

        created = await client.transfers.create(
            team_id=12345,
            data_source_id="AW",
            schema_id=2,
            destination_id=8,
            display_name="AW enhanced",
            schedule=[TransferSchedule(run_interval="daily", run_hour=22)],
            accounts=[TransferAccount(login_id=2682599, account_id="8733197711")],
        )

        runs = await client.transfers.list_runs(
            team_id=12345,
            transfer_id=created.transfer_id,
            start_date=datetime.datetime(2024, 1, 1),
            end_date=datetime.datetime(2024, 1, 31),
        )
        print(f"{len(runs)} runs")


asyncio.run(main())

clone()

Clone an existing transfer, optionally overriding selected fields. The clone is fully independent — editing or deleting it never affects the source. Pass a CloneTransferBody to override fields; omit it to clone as-is.

# Clone as-is
cloned = client.transfers.clone(team_id=12345, transfer_id=36091)
print(cloned.transfer_id, cloned.transfer_name)

# Clone with overrides
from supermetrics import CloneTransferBody

cloned = client.transfers.clone(
    team_id=12345,
    transfer_id=36091,
    overrides=CloneTransferBody(display_name="My clone"),
)
Parameter Type Required Description
team_id int Yes Team identifier
transfer_id int Yes Source transfer to clone
overrides CloneTransferBody \| None No Fields to override in the clone

Returns: TransferCreatedResponse — the new transfer’s ID and display name.

HTTP: POST /teams/{team_id}/transfers/{transfer_id}/clone → 201

batch_create()

Create multiple transfers in a single request. Each configuration is created independently — if one fails, the others still succeed.

from supermetrics import TransferConfigurationRequest

results = client.transfers.batch_create(
    team_id=12345,
    transfers=[config1, config2],
)
print(f"Errors: {results.has_errors}")
for item in results.results:
    print(f"  [{item.index}] {item.status}: {item.transfer_id}")
Parameter Type Required Description
team_id int Yes Team identifier
transfers list[TransferConfigurationRequest] Yes 1–100 transfer configurations

Returns: BatchCreateTransfersResponse200Data — with has_errors flag and per-item results.

HTTP: POST /teams/{team_id}/transfers/batch → 200


TransferRunsResource

Inspect a single execution of a Data Warehouse transfer.

Base URL: As with transfers and backfills, this endpoint lives on the Data Warehouse API and the SDK routes it there automatically — see the base-URL note under BackfillsResource.

Note the path spelling: run lookup is /teams/{team_id}/transfer_runs/{transfer_run_id}, with an underscore, while every other transfer path uses hyphens. That is what the API serves; it matters only if you are matching request paths in a proxy or a test.

get()

Retrieve a transfer run by ID.

run = client.transfer_runs.get(team_id=12345, transfer_run_id=98765)

Parameters:

The run is addressed by its own identifier within the team, so the transfer that produced it does not have to be known. Run ids come from transfers.list_runs().

Returns: TransferRunDetail — status, queued/started/ended timestamps, total duration, row counts, and the per-query execution details

Raises: SupermetricsAuthError, SupermetricsNotFoundError (404 if not found), SupermetricsForbiddenError, SupermetricsAPIError, NetworkError

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    run = client.transfer_runs.get(team_id=12345, transfer_run_id=98765)

    print(f"Status: {run.status}")
    print(f"Rows: {run.total_rows} in {run.total_duration}s")
    print(f"Queries: {run.query_amount} ({run.failed_query_amount} failed)")

    for query in run.query_details:
        print(f"  [{query.status}] {query.rows} rows in {query.duration}s")
        if query.error_description:
            print(f"    {query.error_description}")

Async usage:

import asyncio

from supermetrics import SupermetricsAsyncClient


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        run = await client.transfer_runs.get(team_id=12345, transfer_run_id=98765)
        print(f"Run {run.id}: {run.status}")


asyncio.run(main())

CustomFieldsResource

Define, inspect, and remove a team’s custom fields — the calculated dimensions and metrics that Supermetrics evaluates alongside a data source’s native fields. Upstream calls them field transformations, which is why the models are named TeamTransformationOutput and the generated operations *_transformation.

Base URL: Custom fields are served by the core API host, not by the Data Warehouse API, so nothing is re-hosted for them. The paths keep their /v1 prefix: /v1/teams/{team_id}/custom-fields. A plain client reaches them with no dts_base_url involvement at all.

Each field carries a definition: an ordered pipeline of steps evaluated top to bottom. A step is one of three kinds, discriminated by its type:

Call get_metadata() to discover which functions, rules, and data types the team may actually use before building one.

Request models. Unlike the transfer models, the definition-step types are re-exported from the top-level package — create() and update() cannot be called without constructing them:

from supermetrics import (
    ConditionCase,
    ConditionCaseCondition,
    ConditionStep,
    CustomFieldCreateRequestDataSourceItem,
    DefinitionValue,
    FunctionArgument,
    FunctionStep,
    LookupStep,
    LookupStepMap,
)

The definition is asymmetric between request and response. You send a bare list of steps and you get back an object with an items attribute. A read-modify-write cycle therefore reads field.definition.items and passes that list straight to update() — see the update() example. This is upstream’s shape, not the SDK’s.

Response envelope. All five reading and writing methods are wrapped in {"meta": ..., "data": ...} upstream, and the SDK unwraps .data for you. list() is double-wrapped — the page sits at data.items and the pagination metadata rides in meta — which is why list() returns only the page. See the note under list() for how to read the pagination.

Errors. This domain documents 400, 401, 403, 404, 429, and 500. There is no 422 in it: a rejected definition comes back as HTTP 400, which the SDK translates to SupermetricsValidationError exactly as it does a 422 elsewhere. list() and get_metadata() document no 404 at all.

list()

List the custom fields defined for a team.

fields = client.custom_fields.list(team_id=12345)

Parameters:

Everything from data_source_id onwards is keyword-only.

Returns: list[TeamTransformationOutput] — the custom fields on this page. Empty list when the page has no results, including when the API omits data or data.items entirely.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400 on an invalid query), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Only the parameters you pass are sent. With no optional arguments the query string is empty — limit in particular is not sent, even though the generated layer has a default of 25. The server applies its own default instead, so the SDK never silently pins a page size you did not ask for.

list() returns the page, not the pagination. The total_count, limit, offset, and next/previous links the API sends alongside the page are dropped by the typed method. Read them through the raw-response accessor:

response = client.with_raw_response.custom_fields.list(team_id=12345, include_total_count=True)

fields = response.data  # list[TeamTransformationOutput], same as list() returns
pagination = response.json_body["meta"]["pagination"]

print(f"{len(fields)} of {pagination['total_count']} fields")
print(f"limit={pagination['limit']} offset={pagination['offset']}")

total_count is only present when include_total_count=True. The API omits it by default because counting costs time, so reading it unconditionally will KeyError on a plain call.

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    fields = client.custom_fields.list(team_id=12345, data_source_id="GAWA", limit=50)

    for field in fields:
        print(f"{field.id}: {field.display_name} ({field.field_type}, {field.data_type})")
        print(f"  last modified {field.modified_time_utc} by {field.modified_user.email}")

Paging through everything, using the total to know when to stop:

page = 1
seen = 0

while True:
    response = client.with_raw_response.custom_fields.list(
        team_id=12345, page=page, limit=100, include_total_count=True
    )
    if not response.data:
        break

    for field in response.data:
        print(field.display_name)

    seen += len(response.data)
    if seen >= response.json_body["meta"]["pagination"]["total_count"]:
        break
    page += 1

get()

Retrieve a single custom field by ID.

field = client.custom_fields.get(team_id=12345, custom_field_id=42)

Parameters:

Returns: TeamTransformationOutput — the field with its definition, data type, and last-modified metadata

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    field = client.custom_fields.get(team_id=12345, custom_field_id=42)

    print(f"{field.display_name} [{field.name}] -> {field.data_type}")
    print(f"Applies to {field.data_source_id}, report types {field.report_types}")

    # Note `.definition.items`, not `.definition` — the read shape wraps the list
    for step in field.definition.items:
        print(f"  {step.type_}: {type(step).__name__}")

get_metadata()

Retrieve the building blocks available for custom field definitions.

metadata = client.custom_fields.get_metadata(team_id=12345)

Parameters:

Returns: MetadataOutputData — the functions the team may call, the rules available to condition and lookup steps, the field data types that can be referenced, the output data types a field may declare, and the team’s cap on steps per definition

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Call this before constructing a definition rather than guessing at function and rule names — an unknown name is rejected on create() as a 400.

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    metadata = client.custom_fields.get_metadata(team_id=12345)

    print(f"Up to {metadata.data_transformation_steps_limit} steps per definition")

    for function in metadata.functions.items:
        args = ", ".join(argument["name"] for argument in function.arguments)
        print(f"  [{function.group_name}] {function.name}({args}) -> {function.return_types}")

    print(f"Condition rules: {[rule.name for rule in metadata.rules.condition.items]}")
    print(f"Lookup rules:    {[rule.name for rule in metadata.rules.lookup.items]}")
    print(f"Output types:    {[t.output_type for t in metadata.output_data_types.items]}")

Every attribute on MetadataOutputData is optional upstream, so guard the ones you read if you cannot rely on the team having them.

create()

Create a custom field.

field = client.custom_fields.create(
    team_id=12345,
    display_name="Platform (normalised)",
    field_type="dim",
    data_type="string.text.value",
    definition=[step],
)

Parameters:

description and data_source are keyword-only.

Returns: TeamTransformationOutput — the persisted field, including the id and machine name the API assigned. The API answers HTTP 201 on success.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400 — an invalid definition is a 400, not a 422), SupermetricsNotFoundError, SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example — a three-step pipeline using all three step kinds: upper-case the platform field, map the result to a friendly name, then classify what came out.

from supermetrics import (
    ConditionCase,
    ConditionCaseCondition,
    ConditionStep,
    CustomFieldCreateRequestDataSourceItem,
    DefinitionValue,
    FunctionArgument,
    FunctionStep,
    LookupStep,
    LookupStepMap,
    SupermetricsClient,
)

upper = FunctionStep(
    type_="function",
    name="upper_case",
    arguments=[FunctionArgument(name="value", value=DefinitionValue(type_="data_source_field", value="platform"))],
)

# LookupStepMap takes no mapping in its constructor: `additional_properties` is
# declared init=False, so build it empty and assign the entries.
mapping = LookupStepMap()
mapping["GOOGLE"] = "Google Ads"
mapping["FACEBOOK"] = "Meta Ads"

rename = LookupStep(
    type_="lookup",
    rule="equals",
    map_=mapping,
    source=DefinitionValue(type_="output_from_previous"),
    default=DefinitionValue(type_="static", value="Other"),
)

# ConditionCase's field is `return_` in Python; it serialises to "return", which is a
# Python keyword and so cannot be an attribute name.
classify = ConditionStep(
    type_="condition",
    default=DefinitionValue(type_="static", value="Unclassified"),
    cases=[
        ConditionCase(
            return_=DefinitionValue(type_="static", value="Search"),
            condition=ConditionCaseCondition(
                type_="rule",
                rule="equals",
                source=DefinitionValue(type_="output_from_previous"),
                target=DefinitionValue(type_="static", value="Google Ads"),
            ),
        )
    ],
)

with SupermetricsClient(api_key="your_key") as client:
    field = client.custom_fields.create(
        team_id=12345,
        display_name="Platform (normalised)",
        field_type="dim",
        data_type="string.text.value",
        definition=[upper, rename, classify],
        description="Upper-cases the platform, maps it to a friendly name, then classifies it",
        data_source=[CustomFieldCreateRequestDataSourceItem(data_source_id="GAWA")],
    )
    print(f"Created {field.id} ({field.name})")

update()

Replace an existing custom field.

field = client.custom_fields.update(
    team_id=12345,
    custom_field_id=42,
    display_name="Platform (normalised, v2)",
    data_type="string.text.value",
    definition=[step],
)

Parameters:

Returns: TeamTransformationOutput — the updated field

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400 on an invalid definition), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

update() is not create() with an id attached. Two differences, both upstream’s:

  1. There is no field_type parameter. The field kind cannot be changed after creation, so the request body does not carry it at all. data_source is absent for the same reason.
  2. It is a whole-object replace. There is no PATCH endpoint, so every field listed above is resent on every call and anything you omit reverts to unset — passing no description clears the existing one.

Example — the read-modify-write cycle, which is where the definition asymmetry bites:

from supermetrics import DefinitionValue, FunctionArgument, FunctionStep, SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    current = client.custom_fields.get(team_id=12345, custom_field_id=42)

    # Read: the response wraps the pipeline, so unwrap `.items` to get the list.
    steps = list(current.definition.items)

    # Modify: append a step.
    steps.append(
        FunctionStep(
            type_="function",
            name="trim",
            arguments=[FunctionArgument(name="value", value=DefinitionValue(type_="output_from_previous"))],
        )
    )

    # Write: send the bare list back, and resend everything you want to keep.
    updated = client.custom_fields.update(
        team_id=12345,
        custom_field_id=42,
        display_name=current.display_name,
        data_type=current.data_type,
        definition=steps,
        description=current.description,
    )
    print(f"{updated.display_name} now has {len(updated.definition.items)} steps")

delete()

Delete a custom field.

client.custom_fields.delete(team_id=12345, custom_field_id=42)

Parameters:

Returns: None. The API answers HTTP 204 No Content on success, so there is no body to return and nothing to inspect.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

from supermetrics import SupermetricsClient
from supermetrics.exceptions import SupermetricsNotFoundError

with SupermetricsClient(api_key="your_key") as client:
    try:
        client.custom_fields.delete(team_id=12345, custom_field_id=42)
    except SupermetricsNotFoundError:
        print("Already gone")

Deletion is not idempotent from the caller’s point of view: a second call 404s. Use with_raw_response if you want to assert on the 204 itself.

response = client.with_raw_response.custom_fields.delete(team_id=12345, custom_field_id=42)
assert response.status_code == 204
assert response.data is None

Async usage (all six methods above are also available on CustomFieldsAsyncResource):

import asyncio

from supermetrics import DefinitionValue, FunctionArgument, FunctionStep, SupermetricsAsyncClient


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        metadata, fields = await asyncio.gather(
            client.custom_fields.get_metadata(team_id=12345),
            client.custom_fields.list(team_id=12345, data_source_id="GAWA"),
        )
        print(f"{len(fields)} fields, step limit {metadata.data_transformation_steps_limit}")

        created = await client.custom_fields.create(
            team_id=12345,
            display_name="Platform (upper)",
            field_type="dim",
            data_type="string.text.value",
            definition=[
                FunctionStep(
                    type_="function",
                    name="upper_case",
                    arguments=[
                        FunctionArgument(
                            name="value", value=DefinitionValue(type_="data_source_field", value="platform")
                        )
                    ],
                )
            ],
        )
        await client.custom_fields.delete(team_id=12345, custom_field_id=created.id)


asyncio.run(main())

DestinationsResource

Manage the warehouse and storage destinations that Data Warehouse transfers write into — listing and inspecting them, creating, updating and deleting them, testing a set of credentials before committing to it, and checking which transfers still depend on one.

Base URL: Destination endpoints are served by the Data Warehouse API at https://dts-api.supermetrics.com/v1, not by the core API host. The SDK routes them there for you, exactly as it does for transfers and backfills. See the base-URL note under BackfillsResource for how base_url and dts_base_url interact — in particular, setting base_url yourself disables the automatic routing.

Request models. There are none to import. create(), update(), and test_connection() take the destination-specific configuration as a plain dict[str, Any] in their fields argument, and the SDK converts it for you. The generated CreateDestinationRequestFields, UpdateDestinationRequestFields, and TestConnectionRequestFields classes exist, but they are not constructible: each keeps its storage in an attrs attribute declared init=False, so neither Fields({...}) nor Fields(additional_properties={...}) works. A public signature has to be satisfiable, so the adapter takes the dict and calls .from_dict() internally. Do not go hunting for an import — pass a mapping.

Response envelope. Every method in this resource unwraps a {"meta": ..., "data": ...} envelope and returns the payload itself. Unlike transfers, there is no bare-body case here. The envelope is still visible through client.with_raw_response, which is also the only way to reach meta.request_id.

list()

List the destinations belonging to a team.

destinations = client.destinations.list(team_id=12345)

Parameters:

Returns: list[DestinationListItem] — every destination configured for the team, each carrying id, display_name, and type_. Empty list if the team has none. The endpoint declares no pagination and no filtering parameters, so the whole set arrives in one call.

Raises: SupermetricsAuthError (401), SupermetricsForbiddenError (403), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    for destination in client.destinations.list(team_id=12345):
        print(f"{destination.id}: {destination.display_name} ({destination.type_})")

get()

Retrieve a destination and its editable form settings by ID.

destination = client.destinations.get(team_id=12345, destination_id=8)

Parameters:

Returns: DestinationInfodisplay_name, a destination_type descriptor (type_, title, icon_url, and the type’s own settings and auth methods), edit_settings, and an optional id

Raises: SupermetricsAuthError (401), SupermetricsForbiddenError (403), SupermetricsNotFoundError (404), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

The read shape and the write shape are not the same. get() does not hand back the configuration you posted. It returns edit_settings, a list of SetupSetting UI form descriptors — each one an id / label / value / input_type / is_required record describing a single control — and not the flat fields mapping that create() and update() accept. The SDK surfaces the API’s own model rather than inventing a symmetrical one, so a DestinationInfo cannot be handed straight to update(). To feed an edit back, project the settings yourself, for example with {setting.id: setting.value for setting in destination.edit_settings}, and re-supply anything the API does not hand back.

Example:

destination = client.destinations.get(team_id=12345, destination_id=8)

print(destination.display_name, destination.destination_type.title)
for setting in destination.edit_settings:
    print(f"  {setting.id} ({setting.input_type}): {setting.value}")

create()

Create a new destination.

destination = client.destinations.create(
    team_id=12345,
    type="DWH_SNOWFLAKE",
    display_name="My Snowflake Destination",
    fields={"hostname": "any-domain.my-region.snowflakecomputing.com", "warehouse": "DEMO_WH"},
)

Parameters:

Returns: DestinationInfo — the created destination, in the read shape described under get() above, with edit_settings rather than fields. The API answers HTTP 201 Created on success.

Raises: SupermetricsAuthError (401), SupermetricsValidationError (400 / 422), SupermetricsForbiddenError (403), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

Notes:

Example:

destination = client.destinations.create(
    team_id=12345,
    type="DWH_SNOWFLAKE",
    display_name="My Snowflake Destination",
    fields={
        "hostname": "any-domain.my-region.snowflakecomputing.com",
        "warehouse": "DEMO_WH",
        "database_name": "TEST_DB",
        "schema": "PUBLIC",
        "role": "ACCOUNTADMIN",
        "username": "USER",
        "private_key": "not-a-real-key",
    },
    auth_method="AUTH_METHOD_KEY_PAIR",
)
print(destination.id)

update()

Update an existing destination.

destination = client.destinations.update(
    team_id=12345,
    destination_id=8,
    type="DWH_SNOWFLAKE",
    display_name="Renamed destination",
    fields={"warehouse": "PROD_WH", "database_name": "PROD_DB"},
)

Parameters:

Returns: DestinationInfo, in the read shape described under get()

Raises: SupermetricsAuthError (401), SupermetricsValidationError (400 / 422), SupermetricsForbiddenError (403), SupermetricsNotFoundError (404), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

Notes:

Example:

destination = client.destinations.update(
    team_id=12345,
    destination_id=8,
    type="DWH_SNOWFLAKE",
    display_name="Renamed destination",
    fields={"warehouse": "PROD_WH", "database_name": "PROD_DB"},
    new_password="not-a-real-key",
)
print(destination.display_name)

delete()

Delete a destination.

client.destinations.delete(team_id=12345, destination_id=8)

Parameters:

Returns: None. The API answers HTTP 204 No Content on success.

Raises: SupermetricsAuthError (401), SupermetricsForbiddenError (403), SupermetricsNotFoundError (404), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

Notes:

Example:

from supermetrics import SupermetricsAPIError, SupermetricsNotFoundError

usage = client.destinations.get_usage(team_id=12345, destination_id=8)
if usage.is_used:
    print(f"Still used by {len(usage.transfers)} transfers")
else:
    try:
        client.destinations.delete(team_id=12345, destination_id=8)
    except SupermetricsNotFoundError:
        print("Already gone")
    except SupermetricsAPIError as exc:
        if exc.status_code == 409:
            print("Conflict: something still depends on it")
        else:
            raise

test_connection()

Test destination credentials without saving them.

result = client.destinations.test_connection(
    team_id=12345,
    type="DWH_SNOWFLAKE",
    display_name="Test Connection",
    fields={"hostname": "any-domain.my-region.snowflakecomputing.com", "username": "USER"},
)

Parameters:

Returns: TestConnectionResult with success (bool) and error (str | None)

Raises: SupermetricsAuthError (401), SupermetricsValidationError (400 / 422), SupermetricsForbiddenError (403), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError. A connection that simply does not work is not one of these — see below.

A failed connection test is a returned result, not an exception. The API answers HTTP 200 with success: false and an error message for credentials that do not work; that is a successful call reporting a negative outcome, and test_connection() returns it. Branch on result.success, do not wrap the call in try / except SupermetricsValidationError. This mirrors transfers.validate(). The exceptions listed above are for the call itself failing — bad credential, no access, malformed payload, upstream fault.

Notes:

Example:

result = client.destinations.test_connection(
    team_id=12345,
    type="DWH_SNOWFLAKE",
    display_name="Test Connection",
    fields={
        "hostname": "any-domain.my-region.snowflakecomputing.com",
        "warehouse": "DEMO_WH",
        "username": "USER",
        "private_key": "not-a-real-key",
    },
    auth_method="AUTH_METHOD_KEY_PAIR",
)

if result.success:
    print("Credentials work")
else:
    print(f"Connection failed: {result.error}")

get_usage()

Report which transfers still use a destination.

usage = client.destinations.get_usage(team_id=12345, destination_id=8)

Parameters:

Returns: DestinationUsage with is_used (bool) and transfers, a list of items carrying transfer_id and transfer_name

Raises: SupermetricsAuthError (401), SupermetricsForbiddenError (403), SupermetricsNotFoundError (404), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

Example:

usage = client.destinations.get_usage(team_id=12345, destination_id=8)

if usage.is_used:
    for transfer in usage.transfers:
        print(f"{transfer.transfer_id}: {transfer.transfer_name}")
else:
    client.destinations.delete(team_id=12345, destination_id=8)

batch_update()

Rotate secrets for multiple destinations of the same type in a single request. Each update is applied independently — if one fails, the others still succeed.

from supermetrics import BatchUpdateDestinationsBodyUpdatesItem

results = client.destinations.batch_update(
    team_id=12345,
    destination_type="DWH_SNOWFLAKE",
    updates=[
        BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-new-password"),
        BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-new-password"),
    ],
)

Parameters:

Returns: BatchUpdateDestinationsResponse200Data with has_errors (bool) and results, a list of items carrying destination_id, status ("success" or "error"), and, on failure, error_code and message.

Raises: SupermetricsAuthError (401), SupermetricsForbiddenError (403), SupermetricsValidationError (400), SupermetricsRateLimitError (429), SupermetricsServerError (500), NetworkError

Example:

if results.has_errors:
    for item in results.results:
        if item.status == "error":
            print(f"  destination {item.destination_id} failed: {item.error_code}")

Async usage (all eight methods above are also available on DestinationsAsyncResource):

import asyncio

from supermetrics import SupermetricsAsyncClient


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        destinations = await client.destinations.list(team_id=12345)
        print(f"{len(destinations)} destinations")

        result = await client.destinations.test_connection(
            team_id=12345,
            type="DWH_SNOWFLAKE",
            display_name="Test Connection",
            fields={"hostname": "any-domain.my-region.snowflakecomputing.com", "username": "USER"},
        )
        if not result.success:
            print(f"Connection failed: {result.error}")
            return

        created = await client.destinations.create(
            team_id=12345,
            type="DWH_SNOWFLAKE",
            display_name="My Snowflake Destination",
            fields={"hostname": "any-domain.my-region.snowflakecomputing.com", "username": "USER"},
        )
        print(f"Created {created.id}")


asyncio.run(main())

TableGroupsResource

Manage table groups — the schema definitions that control how data is structured in the warehouse. Table groups define tables, fields, and their mappings for each data source.

The team identity comes from the API key, not a path parameter, so no team_id is needed.

Base URL: https://api.supermetrics.com/enterprise/v2 — Table Groups lives on the core API, not the Data Warehouse host.

list()

List all table groups available for the team.

groups = client.table_groups.list()
for g in groups:
    print(f"{g.group_id} (schema {g.schema_id}): {g.name}")

Returns: list[TableGroup] — each item has group_id (string, e.g. "tg_123"), schema_id (int, for use as schema_id when creating transfers), and name.

export()

Export a table group’s full data model including tables and field mappings.

export = client.table_groups.export(group_id="tg_123", version=1)
print(f"Group: {export.group.group_name} (ds={export.group.ds_id})")
for table in export.tables:
    print(f"  {table.table_name}: {table.fields}")
for field in export.fields:
    print(f"  {field.field_id} -> {field.target_name}")

Args:

Returns: ExportTableGroupResponse200 — has version, group (TableGroupExport), tables (list[TableDefinition]), and fields (list[FieldDefinition]).

import_()

Create a new table group from a data model definition.

from supermetrics import ImportTableGroupBody, TableGroupImport, TableDefinition, FieldDefinition

created = client.table_groups.import_(
    body=ImportTableGroupBody(
        version=1,
        group=TableGroupImport(group_name="My Group", ds_id="AW", table_prefix="MYG"),
        tables=[TableDefinition(table_name="CAMPAIGNS", fields=["campaign_id", "date"])],
        fields=[FieldDefinition(field_id="campaign_id", target_name="campaign_id")],
    )
)
print(f"Created: {created.group_id} ({created.group_name})")

Args:

Returns: TableGroupWriteResponse — the created group with its assigned group_id and group_name.

Raises: ValidationError on invalid data (400), APIError on name conflict (409).

edit()

Update an existing table group (full replace). The natural workflow is export() → edit locally → edit().

from supermetrics import EditTableGroupBody

export = client.table_groups.export(group_id="tg_123", version=1)
updated = client.table_groups.edit(
    group_id="tg_123",
    body=EditTableGroupBody(version=1, group=export.group, tables=export.tables, fields=export.fields),
)

Args:

Returns: TableGroupWriteResponse — the updated group.

Async usage (all four methods above are also available on TableGroupsAsyncResource):

import asyncio
from supermetrics import SupermetricsAsyncClient


async def main():
    async with SupermetricsAsyncClient(api_key="your-key") as client:
        groups = await client.table_groups.list()
        export = await client.table_groups.export(group_id=groups[0].group_id, version=1)
        print(f"{export.group.group_name}: {len(export.tables)} tables")


asyncio.run(main())

BlendsResource

Define, inspect, and remove a team’s blends — the combined tables that draw their rows from several data sources at once. A blend has one of two kinds, fixed at creation and named by blend_type:

Base URL: Blends are served by the core API host, not by the Data Warehouse API, so nothing is re-hosted for them. The paths keep their /v1 prefix: /v1/teams/{team_id}/data-blending/blends. A plain client reaches them with no dts_base_url involvement at all.

Two things describe a blend. blended_data_sources lists the sources it draws on, and config maps each source’s native fields onto the blend’s own fields — and, for a join blend, says how the sources are joined.

Request models. As with custom fields, the request types are re-exported from the top-level package — create() and update() cannot be called without constructing them:

from supermetrics import (
    BlendConfig,
    BlendConfigQueryTable,
    BlendDatasourceFieldRef,
    BlendDatasourceFieldRefMetaType0,
    BlendField,
    BlendJoin,
    BlendJoinCondition,
    BlendJoinJoinTable,
    BlendedDataSourceInput,
    BlendedDataSourceInputAccountsItem,
    BlendedDataSourceInputDataSourceSettingsItem,
    BlendedDataSourceInputReportTypeSettingsItem,
    BlendedDataSourceInputSegmentsItem,
)

BlendedDataSourceInput takes five required arguments, three of which are routinely empty. data_source_id, blend_data_source_id, blend_data_source_key, report_type, and report_type_settings are all required but nullable upstream: they have to be present in the JSON even when there is nothing to say. On a create that means blend_data_source_id=None, and usually report_type=None and report_type_settings=[]. Leaving any of the five out is a TypeError before a request is ever built.

Requests and responses are not the same shape. Every collection is sent as a bare list and comes back wrapped in an object with an items attribute — at every level, so a read goes through blend.blended_data_sources.items, blend.config.fields.items, field.blend_datasource_fields.items, and join.conditions.items. The response also drops blend_data_source_key everywhere, and adds blend_field_type and blend_field_data_type, which upstream infers from the mapped fields and the request has no way to set. A blend cannot be read back and resent unchanged; a read-modify-write has to rebuild the request objects from the response. This is upstream’s shape, not the SDK’s.

blend_data_source_key is a create-time alias for a source that has no id yet. It is exactly eight lowercase alphanumerics (^[a-z0-9]{8}$), and every field and join reference in the same request points at it instead of at an id. On update(), sources that already exist are addressed by blend_data_source_id and sources being added in the same call by a fresh key, so one body legitimately carries both.

list() returns summaries, get() returns whole blends. A BlendListItemOutput has no config at all, and a reduced view of each data source. Call get() for a blend’s fields and joins.

Response envelope. The four methods that return a body are wrapped in {"meta": ..., "data": ...} upstream, and the SDK unwraps .data for you. list() is double-wrapped — the blends sit at data.items — but unlike custom fields there is no pagination to lose here: this endpoint is not paginated, so one call returns every matching blend and returning a bare list drops nothing. with_raw_response still earns its place for the status code, the headers, and meta.request_id.

Errors. This domain documents 400, 401, 403, 429, and 500 on every operation, plus 404 on the three by-id operations — get(), update(), and delete(). list() and create() document no 404 at all; one that arrives anyway is still translated to SupermetricsNotFoundError through the generic path. There is no 422 anywhere in this domain: a rejected blend definition comes back as HTTP 400, which the SDK turns into SupermetricsValidationError exactly as it does a 422 elsewhere. Nothing in the SDK checks that a union blend omits joins or that a join blend supplies query_table — upstream is what rejects those, and it does so with a 400.

list()

List the blends defined for a team.

blends = client.blends.list(team_id=12345)

Parameters:

blend_type is keyword-only.

Returns: list[BlendListItemOutput] — the team’s blends. Empty list when the team has none, including when the API omits data or data.items entirely.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400 on an invalid query), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

There is no pagination here. Upstream returns every matching blend in a single data.items array and documents no page, limit, or cursor — the response uses the plain envelope, whose meta carries only a request id. So there is nothing to page through and nothing for with_raw_response to recover, unlike custom_fields.list().

The summaries carry no config. A list item has blend_id, blend_uuid, type_, display_name, description, modified_time_utc, last_modify_user_email, and a four-attribute view of each data source. Fields and joins exist only on get().

Example:

from supermetrics import SupermetricsClient

with SupermetricsClient(api_key="your_key") as client:
    for summary in client.blends.list(team_id=12345, blend_type="join"):
        sources = summary.blended_data_sources.items
        print(f"{summary.blend_id} [{summary.blend_uuid}] {summary.display_name} ({summary.type_})")
        print(f"  {len(sources)} sources: {', '.join(source.data_source_id for source in sources)}")
        print(f"  modified {summary.modified_time_utc} by {summary.last_modify_user_email}")

get()

Retrieve a single blend, including its full configuration.

blend = client.blends.get(team_id=12345, blend_id=569)

Parameters:

Returns: BlendOutput — the blend, with its data sources and its field and join configuration

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Everything nested here is wrapped. The sources are at blend.blended_data_sources.items, the fields at blend.config.fields.items, a field’s mappings at field.blend_datasource_fields.items, and a join’s conditions at join.conditions.items.

On a union blend, config.query_table and config.joins are Unset, not None. Test them with isinstance(value, Unset). value is None is false for the sentinel, so that check passes silently and the attribute access after it raises.

Example:

from supermetrics import SupermetricsClient
from supermetrics._generated.supermetrics_api_client.types import Unset

with SupermetricsClient(api_key="your_key") as client:
    blend = client.blends.get(team_id=12345, blend_id=569)

    print(f"{blend.display_name} [{blend.blend_uuid}] is a {blend.type_} blend")

    for source in blend.blended_data_sources.items:
        print(f"  source {source.blend_data_source_id}: {source.data_source_id} ({source.display_name})")

    for field in blend.config.fields.items:
        # blend_field_type and blend_field_data_type are response-only — upstream infers
        # them from the mapped fields, and the request has no way to set them.
        print(f"  {field.blend_field_name} ({field.blend_field_type}, {field.blend_field_data_type})")
        for ref in field.blend_datasource_fields.items:
            print(f"    <- {ref.blend_data_source_id}.{ref.datasource_field_name}")

    # A union blend has no joins at all, and the attribute is Unset rather than None.
    if not isinstance(blend.config.joins, Unset):
        for join in blend.config.joins.items:
            print(f"  {join.type_} join on {len(join.conditions.items)} condition(s)")

create()

Create a blend.

blend = client.blends.create(
    team_id=12345,
    display_name="GA4 + Google Ads",
    blend_type="join",
    blended_data_sources=[ga4, ads],
    config=config,
)

Parameters:

description is keyword-only.

Returns: BlendOutput — the persisted blend, including the blend_id and blend_uuid the API assigned and the blend_data_source_id it gave each source. The API answers HTTP 201 on success.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400 — a rejected blend is a 400, not a 422), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Every source is new here, so every source needs a blend_data_source_key. Nothing has an id yet. Pick eight lowercase alphanumerics per source, pass blend_data_source_id=None, and point each BlendDatasourceFieldRef, BlendConfigQueryTable, and BlendJoinJoinTable at that key. The API assigns real ids and answers in those; the keys never appear in a response.

Example — a join blend over two sources, joined on the date:

from supermetrics import (
    BlendConfig,
    BlendConfigQueryTable,
    BlendDatasourceFieldRef,
    BlendField,
    BlendJoin,
    BlendJoinCondition,
    BlendJoinJoinTable,
    BlendedDataSourceInput,
    BlendedDataSourceInputAccountsItem,
    BlendedDataSourceInputDataSourceSettingsItem,
    SupermetricsClient,
)

# One key per new source: exactly eight lowercase alphanumerics, unique in the request.
GA4_KEY = "ga4a0001"
ADS_KEY = "gawa0001"

ga4 = BlendedDataSourceInput(
    data_source_id="GA4",
    blend_data_source_id=None,  # new source: no id yet, so it is named by the key below
    blend_data_source_key=GA4_KEY,
    report_type=None,  # required but nullable, like the two above
    report_type_settings=[],  # required but nullable
    display_name="Google Analytics 4",
    accounts=[BlendedDataSourceInputAccountsItem(account_id="1234567890", account_name="Acme Corp")],
    data_source_settings=[BlendedDataSourceInputDataSourceSettingsItem(id="currency", value="EUR")],
)

ads = BlendedDataSourceInput(
    data_source_id="GAWA",
    blend_data_source_id=None,
    blend_data_source_key=ADS_KEY,
    report_type=None,
    report_type_settings=[],
    display_name="Google Ads",
)

date = BlendField(
    blend_field_name="date",
    blend_field_display_name="Date",
    blend_datasource_fields=[
        BlendDatasourceFieldRef(
            datasource_field_name="Date",
            field_source="standard",
            blend_data_source_key=GA4_KEY,
            datasource_field_type="dim",
            datasource_field_data_type="string.time.date",
        ),
        BlendDatasourceFieldRef(
            datasource_field_name="Date",
            field_source="standard",
            blend_data_source_key=ADS_KEY,
            datasource_field_type="dim",
            datasource_field_data_type="string.time.date",
        ),
    ],
)

impressions = BlendField(
    blend_field_name="impressions",
    blend_field_display_name="Impressions",
    blend_datasource_fields=[
        BlendDatasourceFieldRef(
            datasource_field_name="Impressions",
            field_source="standard",
            blend_data_source_key=ADS_KEY,
            datasource_field_type="met",
            datasource_field_data_type="int.number.value",
        )
    ],
)

config = BlendConfig(
    # A join blend names its primary (left-hand) table and one join per other source.
    query_table=BlendConfigQueryTable(blend_data_source_key=GA4_KEY),
    joins=[
        BlendJoin(
            join_table=BlendJoinJoinTable(blend_data_source_key=ADS_KEY),
            type_="left",
            conditions=[
                BlendJoinCondition(
                    operator="=",
                    left=BlendDatasourceFieldRef(
                        datasource_field_name="Date", field_source="standard", blend_data_source_key=GA4_KEY
                    ),
                    right=BlendDatasourceFieldRef(
                        datasource_field_name="Date", field_source="standard", blend_data_source_key=ADS_KEY
                    ),
                )
            ],
        )
    ],
    fields=[date, impressions],
)

with SupermetricsClient(api_key="your_key") as client:
    blend = client.blends.create(
        team_id=12345,
        display_name="GA4 + Google Ads",
        blend_type="join",
        blended_data_sources=[ga4, ads],
        config=config,
        description="GA4 joined to Google Ads impressions on the date",
    )

    print(f"Created blend {blend.blend_id} ({blend.blend_uuid})")
    for source in blend.blended_data_sources.items:
        # The keys are gone from the response; the API answers in ids from here on.
        print(f"  {source.data_source_id} -> blend_data_source_id={source.blend_data_source_id}")

A union blend is the same call with less in it — no query_table, no joins, and one BlendField per column of the stacked table:

from supermetrics import (
    BlendConfig,
    BlendDatasourceFieldRef,
    BlendField,
    BlendedDataSourceInput,
    SupermetricsClient,
)

GA4_KEY = "ga4a0001"
ADS_KEY = "gawa0001"

sources = [
    BlendedDataSourceInput(
        data_source_id=data_source_id,
        blend_data_source_id=None,
        blend_data_source_key=key,
        report_type=None,
        report_type_settings=[],
    )
    for data_source_id, key in (("GA4", GA4_KEY), ("GAWA", ADS_KEY))
]

config = BlendConfig(
    fields=[
        BlendField(
            blend_field_name="clicks",
            blend_field_display_name="Clicks",
            blend_datasource_fields=[
                BlendDatasourceFieldRef(
                    datasource_field_name="Clicks", field_source="standard", blend_data_source_key=GA4_KEY
                ),
                BlendDatasourceFieldRef(
                    datasource_field_name="Clicks", field_source="standard", blend_data_source_key=ADS_KEY
                ),
            ],
        )
    ]
)

with SupermetricsClient(api_key="your_key") as client:
    blend = client.blends.create(
        team_id=12345,
        display_name="Clicks, everywhere",
        blend_type="union",
        blended_data_sources=sources,
        config=config,
    )
    print(blend.blend_id)

update()

Replace an existing blend.

blend = client.blends.update(
    team_id=12345,
    blend_id=569,
    display_name="GA4 + Google Ads, revised",
    blended_data_sources=[ga4, ads],
    config=config,
)

Parameters:

description is keyword-only.

Returns: BlendOutput — the updated blend. The API answers HTTP 200 on success.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400 on a rejected definition), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

update() is not create() with an id attached. Two differences, both upstream’s:

  1. There is no blend_type parameter. A blend’s kind is fixed at creation, so the request body does not carry it at all.
  2. It is a whole-object replace. There is no PATCH endpoint. display_name, blended_data_sources and config are required and resent in full on every call, so a source left out of blended_data_sources or a field left out of config is dropped from the blend. description is the only optional parameter; omit it and no description key is sent.

One update body may carry both ids and keys. A source that already exists is addressed by blend_data_source_id, with blend_data_source_key=None; a source being added in the same call has no id yet and takes a fresh eight-character key. Field and join references follow the same rule, one attribute or the other per reference.

Example — the read-modify-write cycle, which is where the request/response asymmetry bites: the response objects are a different type from the request objects, so they have to be rebuilt rather than handed back.

from supermetrics import (
    BlendConfig,
    BlendDatasourceFieldRef,
    BlendField,
    BlendedDataSourceInput,
    SupermetricsClient,
)
from supermetrics._generated.supermetrics_api_client.types import Unset

with SupermetricsClient(api_key="your_key") as client:
    current = client.blends.get(team_id=12345, blend_id=569)

    # Read: rebuild the request objects out of the wrapped response. These sources all
    # exist already, so each is addressed by its id and its key is None.
    sources = [
        BlendedDataSourceInput(
            data_source_id=source.data_source_id,
            blend_data_source_id=source.blend_data_source_id,
            blend_data_source_key=None,
            report_type=None if isinstance(source.report_type, Unset) else source.report_type,
            report_type_settings=[],
            display_name=source.display_name,
        )
        for source in current.blended_data_sources.items
    ]

    # blend_field_type and blend_field_data_type come back on every field but have no
    # place in the request, so they are dropped here and re-inferred upstream.
    fields = [
        BlendField(
            blend_field_name=field.blend_field_name,
            blend_field_display_name=field.blend_field_display_name,
            blend_datasource_fields=[
                BlendDatasourceFieldRef(
                    datasource_field_name=ref.datasource_field_name,
                    field_source=ref.field_source,
                    blend_data_source_id=ref.blend_data_source_id,
                    datasource_field_type=ref.datasource_field_type,
                    datasource_field_data_type=ref.datasource_field_data_type,
                )
                for ref in field.blend_datasource_fields.items
            ],
        )
        for field in current.config.fields.items
    ]

    # Modify: rename the blend and keep everything else.
    updated = client.blends.update(
        team_id=12345,
        blend_id=569,
        display_name=f"{current.display_name} (revised)",
        blended_data_sources=sources,
        config=BlendConfig(fields=fields),
        description=current.description,
    )
    print(f"{updated.display_name} now has {len(updated.config.fields.items)} field(s)")

The rebuild above assumes a union blend. A join blend also needs query_table and joins reconstructed, from current.config.query_table and current.config.joins.items, again by id rather than by key.

delete()

Delete a blend.

client.blends.delete(team_id=12345, blend_id=569)

Parameters:

Returns: None. The API answers HTTP 204 No Content on success, so there is no body to return and nothing to inspect.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

from supermetrics import SupermetricsClient
from supermetrics.exceptions import SupermetricsNotFoundError

with SupermetricsClient(api_key="your_key") as client:
    try:
        client.blends.delete(team_id=12345, blend_id=569)
    except SupermetricsNotFoundError:
        print("Already gone")

Deletion is not idempotent from the caller’s point of view: a second call 404s. Use with_raw_response if you want to assert on the 204 itself.

response = client.with_raw_response.blends.delete(team_id=12345, blend_id=569)
assert response.status_code == 204
assert response.data is None

Async usage (all five methods above are also available on BlendsAsyncResource):

import asyncio

from supermetrics import (
    BlendConfig,
    BlendDatasourceFieldRef,
    BlendField,
    BlendedDataSourceInput,
    SupermetricsAsyncClient,
)


async def main():
    async with SupermetricsAsyncClient(api_key="your_key") as client:
        joins, unions = await asyncio.gather(
            client.blends.list(team_id=12345, blend_type="join"),
            client.blends.list(team_id=12345, blend_type="union"),
        )
        print(f"{len(joins)} join blends, {len(unions)} union blends")

        created = await client.blends.create(
            team_id=12345,
            display_name="Clicks, everywhere",
            blend_type="union",
            blended_data_sources=[
                BlendedDataSourceInput(
                    data_source_id="GA4",
                    blend_data_source_id=None,
                    blend_data_source_key="ga4a0001",
                    report_type=None,
                    report_type_settings=[],
                )
            ],
            config=BlendConfig(
                fields=[
                    BlendField(
                        blend_field_name="clicks",
                        blend_field_display_name="Clicks",
                        blend_datasource_fields=[
                            BlendDatasourceFieldRef(
                                datasource_field_name="Clicks",
                                field_source="standard",
                                blend_data_source_key="ga4a0001",
                            )
                        ],
                    )
                ]
            ),
        )
        await client.blends.delete(team_id=12345, blend_id=created.blend_id)


asyncio.run(main())

AccountTagsResource

Group data source accounts from across a team’s connections under a reusable label, so a query or transfer can name the group instead of listing every account by hand. Upstream calls them account groups, which is why the generated operations are named *_account_group.

Base URL: Account tags are served by the core API host, not by the Data Warehouse API, so nothing is re-hosted for them. The paths keep their /v1 prefix: /v1/teams/{team_id}/account_tags. A plain client reaches them with no dts_base_url involvement at all.

Two names, and they are not interchangeable. A tag has a name and a display_name, and mixing them up is the easiest mistake to make here:

Membership is data_sources. The upstream schema declares its elements as an open object with no fixed shape, so the SDK takes and returns plain list[dict[str, Any]] rather than a typed model — nothing new is re-exported from the top-level package for this domain. The documented element form is:

{"data_source_id": "AW", "accounts": [{"account_id": "123-456-7890"}]}

Response envelope. Every response in this domain is wrapped in {"data": ...} upstream — the list, the five single-object methods, and delete() (whose data is {"result": bool}) — and the SDK unwraps .data for you. There is no meta and no pagination — list() returns the whole set (the API caps it at 500), so nothing is lost by unwrapping.

list() and get() return different types. list() yields AccountTagOverview, which summarises membership as data_source_count and account_count; get() yields AccountTag, which carries the data_sources membership itself but no counts.

Errors. This domain documents 400, 401, 403, 429, and 500 on every operation, plus 409 on create() only — the one 409 anywhere in the SDK, raised when a tag with that display name already exists. There is no 404 in this domain at all: an unknown tag comes back as a 400 (translated to SupermetricsValidationError), and deleting one that does not exist is a success — see delete().

list()

List the account tags defined for a team.

tags = client.account_tags.list(team_id=936506)

Parameters:

Returns: list[AccountTagOverview] — the team’s account tags, each with its counts. Empty list when the team has none, including when the API omits data entirely.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

for tag in client.account_tags.list(team_id=936506):
    print(f"{tag.name}: {tag.display_name} ({tag.account_count} accounts)")

get()

Fetch a single account tag by its slug, including its data_sources membership.

tag = client.account_tags.get(team_id=936506, name="a1b2c3d")

Parameters:

Returns: AccountTag — the tag with its data source and account membership.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400 — an unknown tag arrives here, not as a 404), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

tag = client.account_tags.get(team_id=936506, name="a1b2c3d")
for selection in tag.data_sources:
    print(selection["data_source_id"], selection["accounts"])

create()

Create an account tag. The tag’s name is assigned by the server and is not sent in the request — read it off the returned tag.

tag = client.account_tags.create(
    team_id=936506,
    display_name="EMEA paid media",
    color="#112233",
    data_sources=[{"data_source_id": "AW", "accounts": [{"account_id": "123-456-7890"}]}],
)
print(tag.name)  # e.g. "a1b2c3d"

Parameters:

Returns: AccountTag — the created tag, carrying its server-assigned name.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError, and SupermetricsAPIError with status_code == 409 and error_code == "CONFLICT_ERROR" when a tag with that display name already exists

update()

Rename or recolour an account tag. This is the whole of what PUT can change — it does not touch membership. Both fields are required, so a call that means to change only the colour must resend the current display name.

tag = client.account_tags.update(team_id=936506, name="a1b2c3d", display_name="EMEA paid", color="#445566")

Parameters:

Returns: AccountTag — the updated tag.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Membership moves through the PATCH methods, not here. To change which accounts a tag holds, use add_accounts() and remove_accounts().

delete()

Delete an account tag.

existed = client.account_tags.delete(team_id=936506, name="a1b2c3d")

Parameters:

Returns: boolTrue when a tag was deleted, False when no tag of that name existed.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

delete() returns a bool, where every other delete in the SDK returns None. Deletion is idempotent upstream: removing a tag that does not exist is a success, answering HTTP 200 with result=false rather than 404. The boolean is the only place that distinction is recorded. A genuine failure — a bad request, a permission problem, a server error — still raises.

client.account_tags.delete(team_id=936506, name="a1b2c3d")  # True  — removed it
client.account_tags.delete(team_id=936506, name="a1b2c3d")  # False — already gone

add_accounts()

Add data source accounts to a tag. Additive — accounts already in the tag stay in it.

tag = client.account_tags.add_accounts(
    team_id=936506,
    name="a1b2c3d",
    data_sources=[{"data_source_id": "FB", "accounts": [{"account_id": "act_99"}]}],
)

Parameters:

Returns: AccountTag — the tag with its updated membership.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

remove_accounts()

Remove data source accounts from a tag. The tag itself survives even when its last account is removed; use delete() to get rid of it.

tag = client.account_tags.remove_accounts(
    team_id=936506,
    name="a1b2c3d",
    data_sources=[{"data_source_id": "AW", "accounts": [{"account_id": "123-456-7890"}]}],
)

Parameters:

Returns: AccountTag — the tag with its updated membership.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Every method above also accepts the standard per-request auth_token, headers, and timeout keyword-only overrides, and each is mirrored on client.with_raw_response and on the async client.


TeamsResource

Read a team’s identity and its membership. This is a read-only domain: both methods are GET, and there is nothing here that creates, updates, or deletes.

Base URL: Teams are served by the core API host, not by the Data Warehouse API, so nothing is re-hosted for them. The paths keep their /v1 prefix: /v1/teams/{team_id}. A plain client reaches them with no dts_base_url involvement at all.

get()

Fetch a team’s identity by its ID.

team = client.teams.get(team_id=12345)

Parameters:

Returns: TeamData — the team’s identity: team_id, name, display_id, status, and created_at.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsNotFoundError (404 if not found), SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

team = client.teams.get(team_id=12345)
print(f"{team.team_id}: {team.name} ({team.status})")

list_users()

List the members of a team.

users = client.teams.list_users(team_id=12345)

Parameters:

Returns: list[TeamUser] — the team’s members, each with user_id, email, first_name, last_name, role, and created_at.

Raises: SupermetricsAuthError, SupermetricsForbiddenError, SupermetricsValidationError (400), SupermetricsRateLimitError, SupermetricsServerError, NetworkError

Example:

for user in client.teams.list_users(team_id=12345):
    print(f"{user.email} ({user.role})")

Both methods above also accept the standard per-request auth_token, headers, and timeout keyword-only overrides, and each is mirrored on client.with_raw_response and on the async client.


Models

Backfill

Represents a Data Warehouse backfill job.

Attributes:

Example:

backfill = client.backfills.get(team_id=12345, backfill_id=67890)

# Check status
print(f"Status: {backfill.status}")

# Track progress
total = backfill.transfer_runs_total
done = backfill.transfer_runs_completed
failed = backfill.transfer_runs_failed
print(f"Progress: {done}/{total} completed, {failed} failed")

# Check for errors
for err in backfill.error_report:
    print(f"  {err.transfer_run_date}: {err.error}")

TransferBackfillRunError

Represents an error that occurred during a single transfer run within a backfill.

Attributes:

Example:

backfill = client.backfills.get(team_id=12345, backfill_id=67890)
for err in backfill.error_report:
    print(f"Run on {err.transfer_run_date} failed: {err.error}")

DatasourceDetails

Represents complete configuration metadata for a Supermetrics data source.

Key Attributes:

Example:

details = client.datasource_details.get(team_id=12345, data_source_id="GAWA")

print(f"ID:      {details.id}")
print(f"Name:    {details.name}")
print(f"Status:  {details.status}")
print(f"Premium: {details.is_premium}")
print(f"Auth required: {details.is_authentication_required}")

for rt in details.report_types or []:
    print(f"  Report type: {rt.id}{rt.label}")

Represents a data source login link for OAuth authentication.

Key Attributes:

Example:

link = client.login_links.create(ds_id="GAWA")
print(f"Visit: {link.login_url}")
print(f"Expires: {link.expiry_time}")

DataSourceLogin

Represents an authenticated data source login with credentials.

Key Attributes:

Example:

login = client.logins.get(login_id="login_abc123")
print(f"Authenticated as: {login.username}")
print(f"Data Source: {login.ds_info.ds_name}")
print(f"Expires: {login.expiry_time}")

Account

Represents a data source account.

Attributes:

Example:

accounts = client.accounts.list(ds_id="GAWA")
for account in accounts:
    print(f"ID: {account.account_id}")
    print(f"Name: {account.account_name}")
    print(f"Group: {account.group_name}")

DataSourceAccount

The account model returned by logins.get_accounts() — a data source account reachable under a login. Returned by the SDK, not constructed by callers, and not re-exported from the top-level package. Every attribute is optional at the schema level.

Attributes:

Example:

accounts = client.logins.get_accounts("login_abc123")
for account in accounts:
    print(account.account_id, account.name, account.group)

DataResponse

Represents a query execution response.

Key Attributes:

Example:

result = client.queries.execute(...)

# Check metadata
if result and result.meta:
    print(f"Status: {result.meta.status_code}")
    print(f"Request ID: {result.meta.request_id}")

# Process data
if result and result.data:
    columns = [field.field_id for field in result.meta.query.fields]
    print(f"Columns: {columns}")

    for row in result.data:
        print(row)

TransferInfoResponse

Summary of one transfer, as returned by transfers.list(). Not the same shape as TransferConfigurationResponse — see the note under transfers.list() above.

Every field of every transfer model below is optional in the response schema unless marked required, so the generated model defaults it to UNSET. Guard on it rather than assuming presence — see UNSET and Unset.

Attributes:

Example:

for transfer in client.transfers.list(team_id=12345):
    print(f"{transfer.dwh_transfer_id}: {transfer.display_name}")
    print(f"  {transfer.state} / {transfer.schedule} / last run {transfer.run_date}")
    print(f"  accounts: {', '.join(transfer.accounts or [])}")

TransferConfigurationResponse

Full configuration of one transfer, as returned by transfers.get(). The API sends this one bare, without a {"meta": ..., "data": ...} envelope.

Attributes:

Example:

configuration = client.transfers.get(team_id=12345, transfer_id=36091)

print(configuration.display_name)
print(configuration.data_source.data_source_id, configuration.data_source.service_name)

for entry in configuration.schedule or []:
    print(f"{entry.run_interval} @ {entry.run_hour}:00 UTC, window {entry.refresh_window}d")

if configuration.license_ and configuration.license_.is_expired:
    print("License has expired")

TransferCreatedResponse

What transfers.create() returns, unwrapped from its {"meta": ..., "data": ...} envelope.

Attributes:


TransferUpdatedResponse

What transfers.update() returns. The API sends this one bare.

Attributes:


TransferStateUpdateResponse

What transfers.set_state() returns.

Attributes:

Example:

response = client.transfers.set_state(team_id=12345, transfer_id=36091, state="pause")
print(response.result, response.state)  # True PAUSED

ValidationErrorsResponse

What transfers.validate() and transfers.validate_update() return — including when the configuration is invalid, which the API reports as HTTP 200 with is_valid set to False rather than as an error.

Attributes:

Example:

result = client.transfers.validate(...)

if not result.is_valid:
    for error in result.errors or []:
        print(f"{error.field_id}: {error.error_code}")

ValidationError

A single field validation error inside a ValidationErrorsResponse. Unrelated to the ValidationError exception alias exported from supermetrics — this one is a data model and is never raised.

Attributes:

There is no human-readable message in this payload. error_code is all the API gives you, so any text shown to a user has to be mapped from the code by the caller.


AvailableSourcesResponse

What transfers.list_available_sources() returns.

Attributes:

Example:

available = client.transfers.list_available_sources(team_id=12345)

released = [s for s in available.data_sources or [] if s.is_released]
print(f"{len(released)} released data sources")

for destination_type in available.destination_types or []:
    print(f"{destination_type.type_}: {destination_type.title}")

TransferOptionsResponse

What transfers.get_available_options() returns. Almost every member is untyped upstream — see the note under get_available_options().

Attributes:

Example:

options = client.transfers.get_available_options(team_id=12345, source_id="AW", destination_id=8)

print(options.data_source.service_name)
for schema in options.schemas or []:
    print(schema.additional_properties)  # keys are whatever the API sent

TransferRunItem

One run in the list returned by transfers.list_runs(). Leaner than TransferRunDetail: no per-query breakdown, no queued/started timestamps.

Attributes:

Example:

import datetime

runs = client.transfers.list_runs(
    team_id=12345,
    transfer_id=36091,
    start_date=datetime.datetime(2024, 1, 1),
    end_date=datetime.datetime(2024, 1, 31),
)

for run in runs:
    print(f"{run.data_date} [{run.type_}] {run.status}: {run.total_rows} rows")

TransferRunDetail

One run in full, as returned by transfer_runs.get().

Attributes:

Example:

run = client.transfer_runs.get(team_id=12345, transfer_run_id=98765)

print(f"{run.status}: {run.total_rows} rows across {run.query_amount} queries")
print(f"Queued {run.queued_time}, started {run.started_time}, ended {run.ended_time}")

failed = [q for q in run.query_details if q.error_description]
for query in failed:
    print(f"  failed: {query.error_description}")

QueryDetails

Per-query execution details inside a TransferRunDetail.

Attributes:


DataSourceConnection

What transfers.create_datasource_connection() returns, unwrapped from its envelope.

Attributes:

Example:

connection = client.transfers.create_datasource_connection(
    team_id=12345, data_source_id="ADM", destination_type="DWH_SNOWFLAKE"
)
print(str(connection.connection_id))

TransferSchedule

Request-side model. One entry in the schedule list of transfers.create(), update(), validate(), and validate_update(), and also what TransferConfigurationResponse.schedule contains. Every field is optional.

Attributes:

Example:

from supermetrics._generated.supermetrics_api_client.models import TransferSchedule

daily = TransferSchedule(run_interval="daily", run_hour=22, refresh_window=1)
weekly = TransferSchedule(run_interval="weekly", run_hour=3, run_weekday=1)
monthly = TransferSchedule(run_interval="monthly", run_hour=3, run_day=1)

TransferAccount

Request-side model. One entry in the accounts list, and what TransferConfigurationResponse.accounts contains. Every field is optional.

Attributes:

Example:

from supermetrics._generated.supermetrics_api_client.models import TransferAccount

account = TransferAccount(data_source_username="user.name@company.com", login_id=2682599, account_id="8733197711")

TransferSegment

Request-side model. One entry in the optional segments list, and what TransferConfigurationResponse.segments contains. Every field is optional.

Attributes:

Example:

from supermetrics._generated.supermetrics_api_client.models import TransferSegment

segment = TransferSegment(login_id=2830506, segment_id="-1", segment_name="All users")

TransferDataSourceSetting

Request-side model. One entry in the optional data_source_settings list, and what TransferConfigurationResponse.data_source.settings contains. Every field is optional.

Attributes:

Example:

from supermetrics._generated.supermetrics_api_client.models import TransferDataSourceSetting

setting = TransferDataSourceSetting(field_id="BRAND_KEYWORDS", value="", group="Default")

TeamTransformationOutput

A persisted custom field, as every read and write on client.custom_fields returns it. Named after the upstream term: a custom field is a field transformation.

Every attribute is optional upstream, so each is typed ... | Unset.

Attributes:

Example:

import datetime

field = client.custom_fields.get(team_id=12345, custom_field_id=42)

print(f"{field.display_name} ({field.name}) -> {field.data_type}")
print(f"{'Dimension' if field.field_type == 'dim' else 'Metric'} on {field.data_source_id}")

# modified_time_utc is tz-aware, so it compares directly against an aware datetime
age = datetime.datetime.now(datetime.UTC) - field.modified_time_utc
print(f"Last touched {age.days} days ago by {field.modified_user.email}")

for step in field.definition.items:
    print(f"  {step.type_}")

TeamTransformationOutputDefinition

The read-side wrapper around a definition. Its only reason to exist is that the response nests the pipeline in an object while the request takes a bare list — which is why a read-modify-write reads .items and passes that straight back to update().

Attributes:

The union is a oneOf upstream and is discriminated correctly on parse: a definition holding all three kinds comes back as [FunctionStep, LookupStep, ConditionStep], in the order the API sent them. Branch on type(step) or on step.type_.

Example:

from supermetrics import ConditionStep, FunctionStep, LookupStep

field = client.custom_fields.get(team_id=12345, custom_field_id=42)

for step in field.definition.items:
    if isinstance(step, FunctionStep):
        print(f"call {step.name} with {len(step.arguments)} argument(s)")
    elif isinstance(step, LookupStep):
        print(f"lookup by {step.rule} over {len(step.map_.additional_properties)} entries")
    elif isinstance(step, ConditionStep):
        print(f"{len(step.cases)} case(s), default {step.default}")

MetadataOutputData

What custom_fields.get_metadata() returns: the building blocks a team is allowed to use in a definition. Every attribute is optional upstream.

Attributes:

Note the repeated .items indirection — functions, output_data_types, rules.condition, and rules.lookup are each a wrapper object, not a list. Only field_data_types is a bare list.

An argument inside FunctionSpecificationOutput.arguments is an open-ended object with no declared fields, so read it by key (argument["name"]), not by attribute.

Example:

metadata = client.custom_fields.get_metadata(team_id=12345)

by_group: dict[str, list[str]] = {}
for function in metadata.functions.items:
    by_group.setdefault(function.group_name, []).append(function.name)

for group, names in sorted(by_group.items()):
    print(f"{group}: {', '.join(sorted(names))}")

print(f"Legal data_type values: {[t.output_type for t in metadata.output_data_types.items]}")
print(f"Lookup rules: {[rule.name for rule in metadata.rules.lookup.items]}")
print(f"At most {metadata.data_transformation_steps_limit} steps per definition")

FunctionStep

A definition step that applies a named function to its arguments. Importable from supermetrics.

Attributes:

Example:

from supermetrics import DefinitionValue, FunctionArgument, FunctionStep

step = FunctionStep(
    type_="function",
    name="upper_case",
    arguments=[FunctionArgument(name="value", value=DefinitionValue(type_="data_source_field", value="platform"))],
    description="Normalise casing before the lookup",
)

LookupStep

A definition step that maps input values to output values through a lookup table and a matching rule. Importable from supermetrics.

Attributes:

Example:

from supermetrics import DefinitionValue, LookupStep, LookupStepMap

mapping = LookupStepMap()
mapping["GOOGLE"] = "Google Ads"
mapping["FACEBOOK"] = "Meta Ads"

step = LookupStep(
    type_="lookup",
    rule="equals",
    map_=mapping,
    source=DefinitionValue(type_="output_from_previous"),
    default=DefinitionValue(type_="static", value="Other"),
)

LookupStepMap

The key/value table inside a LookupStep. Importable from supermetrics.

Upstream declares it as an open-ended object with no fixed properties, so the generated model has no declared attributes at all — the mapping lives in additional_properties, which is declared init=False.

The constructor takes no mapping. LookupStepMap({"a": "b"}) is a TypeError. Build it empty and assign the entries, either through the item accessors or by replacing additional_properties wholesale.

Example:

from supermetrics import LookupStepMap

# Item assignment
mapping = LookupStepMap()
mapping["GOOGLE"] = "Google Ads"
mapping["FACEBOOK"] = "Meta Ads"

# Or assign the whole dict at once
bulk = LookupStepMap()
bulk.additional_properties = {"GOOGLE": "Google Ads", "FACEBOOK": "Meta Ads"}

# Reading one back
print(mapping["GOOGLE"])  # "Google Ads"
print("TIKTOK" in mapping)  # False
print(mapping.additional_properties)  # {'GOOGLE': 'Google Ads', 'FACEBOOK': 'Meta Ads'}

ConditionStep

A definition step that evaluates an ordered list of cases and produces the result of the first match, falling back to default when none match. Importable from supermetrics.

Attributes:

Example:

from supermetrics import ConditionCase, ConditionCaseCondition, ConditionStep, DefinitionValue

step = ConditionStep(
    type_="condition",
    default=DefinitionValue(type_="static", value="Unclassified"),
    cases=[
        ConditionCase(
            return_=DefinitionValue(type_="static", value="Search"),
            condition=ConditionCaseCondition(
                type_="rule",
                rule="equals",
                source=DefinitionValue(type_="output_from_previous"),
                target=DefinitionValue(type_="static", value="Google Ads"),
            ),
        )
    ],
)

ConditionCase

One case inside a ConditionStep: when condition evaluates true, the return value is produced. Importable from supermetrics.

Attributes:

The field is return_, with a trailing underscore. It serializes to "return" on the wire, which cannot be a Python attribute name because return is a keyword. This is the same convention as type_ and map_ elsewhere in these models.

Example:

from supermetrics import ConditionCase, ConditionCaseCondition, DefinitionValue

case = ConditionCase(
    return_=DefinitionValue(type_="static", value="Paid"),
    condition=ConditionCaseCondition(
        type_="rule",
        rule="equals",
        source=DefinitionValue(type_="data_source_field", value="medium"),
        target=DefinitionValue(type_="static", value="cpc"),
    ),
)

ConditionCaseCondition

The rule-based comparison inside a ConditionCase. Importable from supermetrics.

Attributes:

Example:

from supermetrics import ConditionCaseCondition, DefinitionValue

condition = ConditionCaseCondition(
    type_="rule",
    rule="equals",
    source=DefinitionValue(type_="output_from_previous"),
    target=DefinitionValue(type_="static", value="1"),
)

DefinitionValue

A value reference used throughout a definition — as a function argument, a lookup source or default, and both sides of a condition. Importable from supermetrics.

Attributes:

Example:

from supermetrics import DefinitionValue

from_field = DefinitionValue(type_="data_source_field", value="platform")
from_previous = DefinitionValue(type_="output_from_previous")  # no value
literal = DefinitionValue(type_="static", value="cpc")

FunctionArgument

A named argument supplied to a FunctionStep. Importable from supermetrics.

Attributes:

Example:

from supermetrics import DefinitionValue, FunctionArgument

argument = FunctionArgument(name="value", value=DefinitionValue(type_="data_source_field", value="platform"))

CustomFieldCreateRequestDataSourceItem

One entry in the optional data_source list on custom_fields.create(), scoping the field to a data source and optionally to a report type within it. Importable from supermetrics. Both attributes are optional.

Attributes:

update() does not accept this — the data sources a field applies to are fixed at creation, like field_type.

Example:

from supermetrics import CustomFieldCreateRequestDataSourceItem

sources = [
    CustomFieldCreateRequestDataSourceItem(data_source_id="GAWA"),
    CustomFieldCreateRequestDataSourceItem(data_source_id="AW", report_type="Default"),
]

DestinationListItem

One destination as it appears in destinations.list(). Deliberately thin: the list endpoint returns an index, not a configuration. Call destinations.get() for the rest.

Attributes:

The attribute is type_, with a trailing underscore. The wire field is type, and the SDK method parameter is type, but type is a Python builtin so the generated model renames the attribute. You write type=, you read .type_.

Example:

for destination in client.destinations.list(team_id=12345):
    print(f"{destination.id}: {destination.display_name} ({destination.type_})")

DestinationInfo

A single destination, as returned by destinations.get(), destinations.create() and destinations.update().

Attributes:

This is the read shape, and it is not the write shape. create() and update() take a flat fields mapping; what comes back is edit_settings, a list of form descriptors. You cannot round-trip a DestinationInfo straight back into update() — project it first, with something like {s.id: s.value for s in info.edit_settings}. Secret settings come back with value null.

Example:

info = client.destinations.get(team_id=12345, destination_id=8)

print(f"{info.display_name} ({info.destination_type.title})")
for setting in info.edit_settings:
    print(f"  {setting.label or setting.id} = {setting.value}")

DestinationType

Describes a kind of destination — Snowflake, BigQuery and so on — including the settings it accepts and the authentication methods it supports. Reached through DestinationInfo.destination_type.

Attributes:


SetupSetting

One configuration setting, described rather than merely valued: enough to render an input for it and to know whether it is required. Appears in DestinationInfo.edit_settings and in DestinationType.settings.

Attributes:

Example:

info = client.destinations.get(team_id=12345, destination_id=8)

required_but_empty = [s.id for s in info.edit_settings if s.is_required and s.value in (None, "")]

AuthMethod

An authentication method supported by a destination type. Reached through DestinationType.auth_methods. Its id is what you pass as auth_method.

Attributes:


TestConnectionResult

The verdict from destinations.test_connection().

Attributes:

A failed test is a return value, not an exception. The API answers HTTP 200 with success set to False, so test_connection() returns the result rather than raising. Check success — an exception here means the request itself failed.

Example:

result = client.destinations.test_connection(
    team_id=12345,
    type="DWH_SNOWFLAKE",
    display_name="Preflight",
    fields={"hostname": "any-domain.my-region.snowflakecomputing.com"},
)
if not result.success:
    print(f"Cannot connect: {result.error}")

DestinationUsage

Which transfers write to a destination, as returned by destinations.get_usage(). Worth calling before an update or a delete: changing a destination changes every pipeline below it.

Attributes:

Example:

usage = client.destinations.get_usage(team_id=12345, destination_id=8)
if usage.is_used:
    names = ", ".join(t.transfer_name for t in usage.transfers)
    raise RuntimeError(f"Still in use by: {names}")

client.destinations.delete(team_id=12345, destination_id=8)

DestinationUsageTransfersItem

One transfer in a DestinationUsage report.

Attributes:


BlendOutput

A whole blend, as blends.get(), blends.create(), and blends.update() return it. Every attribute is optional upstream, so each is typed ... | Unset.

Attributes:

This object cannot be handed back to update(). It is a different type from the request models at every level: collections are wrapped instead of bare, blend_data_source_key is gone, and blend_field_type / blend_field_data_type have been added. A read-modify-write rebuilds BlendedDataSourceInput and BlendConfig objects from it — see the update() example.

Example:

import datetime

blend = client.blends.get(team_id=12345, blend_id=569)

print(f"{blend.display_name} ({blend.type_}) — {blend.blend_uuid}")

# modified_time_utc is tz-aware, so it compares directly against an aware datetime
age = datetime.datetime.now(datetime.UTC) - blend.modified_time_utc
print(f"Last touched {age.days} days ago by {blend.last_modify_user_email}")

for source in blend.blended_data_sources.items:
    print(f"  {source.data_source_id} (blend_data_source_id={source.blend_data_source_id})")

BlendListItemOutput

A blend summary, as blends.list() returns them. Every attribute is optional upstream.

Attributes:

A summary is not a small BlendOutput. It has no config — no fields, no joins, no query table — and its data sources carry four attributes rather than ten. Anything beyond the list above requires a get() per blend.

Example:

summaries = client.blends.list(team_id=12345)

# The summary is enough to pick one; the fields need a second call.
for summary in summaries:
    print(f"{summary.blend_id}: {summary.display_name} ({summary.type_})")

blend = client.blends.get(team_id=12345, blend_id=summaries[0].blend_id)
print([field.blend_field_name for field in blend.config.fields.items])

BlendListDataSourceOutput

The reduced view of a data source that appears inside a BlendListItemOutput. Every attribute is optional upstream.

Attributes:

Settings, accounts, segments, and report type are absent here; they are on BlendedDataSourceOutput, which only get(), create(), and update() return.

Example:

for summary in client.blends.list(team_id=12345):
    names = [source.display_name for source in summary.blended_data_sources.items]
    print(f"{summary.display_name}: {', '.join(names)}")

BlendConfigOutput

The read side of a blend’s configuration. Every attribute is optional upstream.

Attributes:

On a union blend, query_table and joins are Unset, not None. They are absent from the JSON rather than null, so the generated model leaves the sentinel in place. Guard them with isinstance(config.joins, Unset); config.joins is None is false and the .items access after it raises AttributeError.

Example:

from supermetrics._generated.supermetrics_api_client.types import Unset

config = client.blends.get(team_id=12345, blend_id=569).config

for field in config.fields.items:
    print(field.blend_field_name)

if isinstance(config.joins, Unset):
    print("union blend: no joins, no query table")
else:
    print(f"join blend on source {config.query_table.blend_data_source_id}")
    for join in config.joins.items:
        print(f"  {join.type_} join with {join.join_table.blend_data_source_id}")

BlendFieldOutput

One field of a blend, on the read side: the blend’s own column plus the per-source fields that feed it. Every attribute is optional upstream.

Attributes:

Two of these five attributes have no request counterpart. BlendField has blend_field_name, blend_field_display_name, and blend_datasource_fields and nothing else, so blend_field_type and blend_field_data_type are dropped when rebuilding a field for update() and re-inferred upstream.

Example:

blend = client.blends.get(team_id=12345, blend_id=569)

for field in blend.config.fields.items:
    kind = "dimension" if field.blend_field_type == "dim" else "metric"
    print(f"{field.blend_field_display_name}{kind}, {field.blend_field_data_type}")
    for ref in field.blend_datasource_fields.items:
        print(f"  source {ref.blend_data_source_id}: {ref.datasource_field_name}")

BlendDatasourceFieldRefOutput

The read side of a per-source field reference. Every attribute is optional upstream.

Attributes:

There is no blend_data_source_key here. The key is a request-scoped alias, so the response only ever speaks in blend_data_source_id. This is the read counterpart of BlendDatasourceFieldRef, which has both.

Example:

from supermetrics._generated.supermetrics_api_client.types import Unset

field = client.blends.get(team_id=12345, blend_id=569).config.fields.items[0]

for ref in field.blend_datasource_fields.items:
    print(f"{ref.datasource_field_name} ({ref.field_source}) from {ref.blend_data_source_id}")
    if ref.meta is not None and not isinstance(ref.meta, Unset):
        print(f"  meta: {ref.meta.additional_properties}")

BlendJoinOutput

The read side of one join between the primary table and another source. Every attribute is optional upstream.

Attributes:

Example:

config = client.blends.get(team_id=12345, blend_id=569).config

for join in config.joins.items:
    print(f"{join.type_} join with source {join.join_table.blend_data_source_id}")
    for condition in join.conditions.items:
        left, right = condition.left, condition.right
        print(
            f"  {left.blend_data_source_id}.{left.datasource_field_name}"
            f" {condition.operator} "
            f"{right.blend_data_source_id}.{right.datasource_field_name}"
        )

BlendedDataSourceOutput

The read side of one data source in a blend, as it appears at blend.blended_data_sources.items. Every attribute is optional upstream.

Attributes:

The settings, accounts, and segments items have no declared attributes. Upstream models them as free-form objects on the response side only, so the generated classes expose nothing but additional_properties — read them by key, not by attribute: source.accounts.items[0].additional_properties["account_id"]. The request side is typed: BlendedDataSourceInput takes real BlendedDataSourceInputAccountsItem objects with .account_id and .account_name.

Example:

blend = client.blends.get(team_id=12345, blend_id=569)

for source in blend.blended_data_sources.items:
    print(f"{source.display_name} ({source.data_source_id}), report type {source.report_type}")

    for setting in source.data_source_settings.items:
        # Untyped on the way out: everything is in additional_properties.
        properties = setting.additional_properties
        print(f"  {properties['id']} = {properties['value']!r}")

    for account in source.accounts.items:
        print(f"  account {account.additional_properties['account_id']}")

BlendedDataSourceInput

One data source in a create() or update() request. Importable from supermetrics.

Attributes:

The first five arguments are required but nullable, and three of them are usually empty. Upstream marks them required and nullable: they must appear in the JSON even when there is nothing to say. So a create typically reads blend_data_source_id=None, blend_data_source_key="abcd1234", report_type=None, report_type_settings=[]. They have no defaults in the generated model, so omitting one is a TypeError, not a silently absent key.

Exactly one of the id and the key identifies the source. A new source has a key and a None id; an existing one has an id and a None key. Field and join references in the same request must point at whichever of the two the source was given.

Example:

from supermetrics import (
    BlendedDataSourceInput,
    BlendedDataSourceInputAccountsItem,
    BlendedDataSourceInputDataSourceSettingsItem,
    BlendedDataSourceInputSegmentsItem,
)

# New source, named by a key for the rest of the request
new_source = BlendedDataSourceInput(
    data_source_id="GA4",
    blend_data_source_id=None,
    blend_data_source_key="abcd1234",
    report_type=None,
    report_type_settings=[],
    display_name="Google Analytics 4",
    data_source_settings=[
        BlendedDataSourceInputDataSourceSettingsItem(id="currency", value="EUR"),
        BlendedDataSourceInputDataSourceSettingsItem(id="row_limit", value=1000),
        BlendedDataSourceInputDataSourceSettingsItem(id="include_empty", value=False),
        BlendedDataSourceInputDataSourceSettingsItem(id="timezone", value=None),
    ],
    accounts=[BlendedDataSourceInputAccountsItem(account_id="1234567890", account_name="Acme Corp")],
    segments=[BlendedDataSourceInputSegmentsItem(id="organic_traffic", name="Organic Traffic")],
)

# Existing source on an update, addressed by its id
existing_source = BlendedDataSourceInput(
    data_source_id="GAWA",
    blend_data_source_id=146715,
    blend_data_source_key=None,
    report_type=None,
    report_type_settings=[],
)

BlendConfig

The write side of a blend’s configuration, passed to create() and update(). Importable from supermetrics. Every attribute is optional.

Attributes:

Nothing here enforces the union/join distinction. Upstream models this as one loose object with all three attributes optional rather than as a discriminated union, so the generated layer will happily send joins on a union blend or a join blend with no query_table. The API is what rejects those, with an HTTP 400.

Example:

from supermetrics import BlendConfig, BlendConfigQueryTable, BlendField

# Union blend: fields only
union_config = BlendConfig(fields=[BlendField(blend_field_name="clicks", blend_datasource_fields=[])])

# Join blend: fields, plus a primary table and one join per other source
join_config = BlendConfig(
    fields=[BlendField(blend_field_name="clicks", blend_datasource_fields=[])],
    query_table=BlendConfigQueryTable(blend_data_source_key="abcd1234"),
    joins=[],
)

BlendField

The write side of one blend field: the blend’s own column and the per-source fields that feed it. Importable from supermetrics.

Attributes:

There is no way to set the field’s type or data type. Upstream infers both from the mapped source fields and returns them on BlendFieldOutput.

Example:

from supermetrics import BlendDatasourceFieldRef, BlendField

# A union blend maps the same logical column across every source
clicks = BlendField(
    blend_field_name="clicks",
    blend_field_display_name="Clicks",
    blend_datasource_fields=[
        BlendDatasourceFieldRef(
            datasource_field_name="Clicks", field_source="standard", blend_data_source_key="abcd1234"
        ),
        BlendDatasourceFieldRef(
            datasource_field_name="Clicks", field_source="standard", blend_data_source_key="efgh5678"
        ),
    ],
)

BlendDatasourceFieldRef

A reference to one field inside one data source, used both in a BlendField mapping and on each side of a BlendJoinCondition. Importable from supermetrics.

Attributes:

Set the id or the key, never neither. At least one of blend_data_source_id and blend_data_source_key must be non-null, and it has to match how the source itself was identified in blended_data_sources. Both default to UNSET, so a reference that names no source is constructible and is rejected upstream with a 400.

Example:

from supermetrics import BlendDatasourceFieldRef

# On create, every source is new, so references point at keys
by_key = BlendDatasourceFieldRef(
    datasource_field_name="Date",
    field_source="standard",
    blend_data_source_key="abcd1234",
    datasource_field_type="dim",
    datasource_field_data_type="string.time.date",
)

# On update, a source that already exists is pointed at by id
by_id = BlendDatasourceFieldRef(
    datasource_field_name="Date",
    field_source="standard",
    blend_data_source_id=146715,
)

BlendDatasourceFieldRefMetaType0

The optional meta object on a BlendDatasourceFieldRef. Importable from supermetrics.

Upstream declares it as an open-ended object with no fixed properties, so the generated model has no declared attributes at all — the contents live in additional_properties, which is declared init=False.

The constructor takes no mapping. BlendDatasourceFieldRefMetaType0({"a": "b"}) is a TypeError. Build it empty and assign the entries, either through the item accessors or by replacing additional_properties wholesale — the same shape as LookupStepMap.

On the read side, meta is parsed by a try/except cascade: a JSON null becomes None and an object becomes a BlendDatasourceFieldRefOutputMetaType0, again with everything in .additional_properties.

Example:

from supermetrics import BlendDatasourceFieldRef, BlendDatasourceFieldRefMetaType0

meta = BlendDatasourceFieldRefMetaType0()
meta["account_id"] = "1234567890"

ref = BlendDatasourceFieldRef(
    datasource_field_name="Custom Conversion",
    field_source="data_source_account_custom",
    blend_data_source_key="abcd1234",
    meta=meta,
)

BlendJoin

One join between the primary table and another source, in a join blend’s BlendConfig. Importable from supermetrics. All three attributes are required.

Attributes:

A join blend needs one BlendJoin per source beyond the primary table. Union blends have none, and sending one is an upstream 400.

Example:

from supermetrics import BlendDatasourceFieldRef, BlendJoin, BlendJoinCondition, BlendJoinJoinTable

join = BlendJoin(
    join_table=BlendJoinJoinTable(blend_data_source_key="efgh5678"),
    type_="left",
    conditions=[
        BlendJoinCondition(
            operator="=",
            left=BlendDatasourceFieldRef(
                datasource_field_name="Date", field_source="standard", blend_data_source_key="abcd1234"
            ),
            right=BlendDatasourceFieldRef(
                datasource_field_name="Date", field_source="standard", blend_data_source_key="efgh5678"
            ),
        )
    ],
)

BlendJoinCondition

One equality between a field on each side of a BlendJoin. Importable from supermetrics. All three attributes are required.

Attributes:

Both sides are full BlendDatasourceFieldRef objects, so each names its own source by id or by key.

Example:

from supermetrics import BlendDatasourceFieldRef, BlendJoinCondition

condition = BlendJoinCondition(
    operator="=",
    left=BlendDatasourceFieldRef(
        datasource_field_name="Campaign ID", field_source="standard", blend_data_source_key="abcd1234"
    ),
    right=BlendDatasourceFieldRef(
        datasource_field_name="Campaign ID", field_source="standard", blend_data_source_key="efgh5678"
    ),
)

BlendJoinJoinTable

Names the source on the right-hand side of a BlendJoin. Importable from supermetrics. Both attributes are optional, but one of them has to be set.

Attributes:

Example:

from supermetrics import BlendJoinJoinTable

on_create = BlendJoinJoinTable(blend_data_source_key="efgh5678")
on_update = BlendJoinJoinTable(blend_data_source_id=146715)

BlendConfigQueryTable

Names the primary (left-hand) source of a join blend, at config.query_table. Importable from supermetrics. Both attributes are optional, but one of them has to be set.

Attributes:

Union blends leave config.query_table out entirely, and it comes back Unset on the read side rather than None.

Example:

from supermetrics import BlendConfig, BlendConfigQueryTable, BlendField

config = BlendConfig(
    query_table=BlendConfigQueryTable(blend_data_source_key="abcd1234"),
    joins=[],
    fields=[BlendField(blend_field_name="clicks", blend_datasource_fields=[])],
)

AccountTagOverview

The list-item projection returned by account_tags.list(). It summarises a tag’s membership as counts rather than listing it. Returned by the SDK, not constructed by callers. Every attribute is optional at the schema level.

Attributes:


AccountTag

The single-object model returned by account_tags.get(), create(), update(), add_accounts(), and remove_accounts(). Unlike AccountTagOverview it carries the data_sources membership rather than counts. Returned by the SDK, not constructed by callers. Every attribute is optional at the schema level.

Attributes:


Exceptions

All exceptions inherit from SupermetricsError.

SupermetricsError
├── SupermetricsClientError        local configuration problems; also a ValueError
├── NetworkError                   timeout, connection refused, DNS, TLS
└── SupermetricsAPIError                           [alias: APIError]
    ├── SupermetricsAuthError       401            [alias: AuthenticationError]
    ├── SupermetricsForbiddenError  403
    ├── SupermetricsNotFoundError   404
    ├── SupermetricsValidationError 400 / 422      [alias: ValidationError]
    ├── SupermetricsRateLimitError  429
    └── SupermetricsServerError     5xx

Behaviour change: AuthenticationError and ValidationError are still importable and are now aliases of SupermetricsAuthError and SupermetricsValidationError — which are subclasses of APIError. An except APIError clause placed before except AuthenticationError now matches authentication errors first and leaves the later clause unreachable. Order the specific classes before APIError. except SupermetricsError is unaffected.

from supermetrics import APIError, SupermetricsAuthError, SupermetricsRateLimitError

try:
    logins = client.logins.list()
except SupermetricsAuthError:
    ...  # 401 only
except SupermetricsRateLimitError:
    ...  # 429 only
except APIError:
    ...  # everything else at the HTTP layer

SupermetricsError

Base exception for all SDK errors.

Attributes:

Example:

from supermetrics import SupermetricsError

try:
    client.accounts.list(ds_id="GAWA")
except SupermetricsError as e:
    print(f"Error: {e.message}")
    print(f"Status: {e.status_code}")
    print(f"Endpoint: {e.endpoint}")

SupermetricsClientError

Raised for client-side configuration and validation errors, detected locally before any HTTP request is made. Also inherits from ValueError, so existing code that catches ValueError around client construction keeps working.

Common Causes:

Example:

from supermetrics import SupermetricsClient, SupermetricsClientError

try:
    client = SupermetricsClient()  # no credential supplied
except SupermetricsClientError as e:
    print(f"Configuration problem: {e.message}")

NetworkError

Raised for network-level failures, before any HTTP response is received. Has no status_code for that reason.

Common Causes:

Example:

from supermetrics import NetworkError, SupermetricsClient

try:
    client = SupermetricsClient(api_key="key", timeout=1.0)
    client.login_links.list()
except NetworkError as e:
    print(f"Network error: {e.message}")

SupermetricsAPIError

Alias: APIError. Base class for every HTTP-level error (4xx and 5xx). Catching it also catches all of the status-specific subclasses below.

Attributes:

Properties:

Example:

from supermetrics import APIError

try:
    client.logins.get("nonexistent_id")
except APIError as e:
    print(f"{e.status_code} {e.error_code}: {e.error_message}")
    print(f"Request ID: {e.request_id}")

SupermetricsAuthError

Alias: AuthenticationError. Raised when authentication fails (HTTP 401).

Common Causes:

A 401 carries the upstream OAuth code in error_code, so a caller can tell “refresh and retry” apart from “this credential will never work”.

Example:

from supermetrics import SupermetricsAuthError

try:
    logins = client.logins.list()
except SupermetricsAuthError as e:
    if e.error_code in ("ACCESS_TOKEN_INVALID", "ACCESS_TOKEN_EXPIRED"):
        credentials.refresh()
        logins = client.logins.list()
    else:
        raise

SupermetricsForbiddenError

Raised when the caller is authenticated but not permitted (HTTP 403). Typically the token lacks the required scope, or the account has no access to the requested team or resource.

Example:

from supermetrics import SupermetricsForbiddenError

try:
    client.datasource_details.get(team_id=12345, data_source_id="GAWA")
except SupermetricsForbiddenError as e:
    print(f"Missing permission: {e.error_message}")

SupermetricsNotFoundError

Raised when the requested resource does not exist (HTTP 404).

Example:

from supermetrics import SupermetricsNotFoundError

try:
    client.logins.get("nonexistent_id")
except SupermetricsNotFoundError:
    print("Login not found")

SupermetricsValidationError

Alias: ValidationError. Raised when request validation fails (HTTP 400 or 422).

Common Causes:

Example:

from supermetrics import SupermetricsValidationError

try:
    client.accounts.list(ds_id="")  # Invalid empty ds_id
except SupermetricsValidationError as e:
    print(f"Validation failed: {e.error_message}")
    print(f"Details: {e.details}")

SupermetricsRateLimitError

Raised when the API rate limit is exceeded (HTTP 429). Use retry_after to find out how long to wait.

Example:

import time

from supermetrics import SupermetricsRateLimitError

try:
    accounts = client.accounts.list(ds_id="GAWA")
except SupermetricsRateLimitError as e:
    time.sleep(e.retry_after or 30)
    accounts = client.accounts.list(ds_id="GAWA")

SupermetricsServerError

Raised when the API reports a server-side failure (HTTP 5xx).

Example:

from supermetrics import SupermetricsServerError

try:
    client.queries.get_results(query_id="query_abc123")
except SupermetricsServerError as e:
    print(f"Upstream failure {e.status_code}, span {e.span_id}")

Type Utilities

TokenProvider

Type alias for the credential callable accepted by SupermetricsClient: Callable[[], str]. It is invoked once per request, so a long-lived client can follow a short-lived token without discarding its connection pool.

Example:

import os

from supermetrics import SupermetricsClient, TokenProvider


def read_token() -> str:
    return os.environ["SUPERMETRICS_TOKEN"]


provider: TokenProvider = read_token
client = SupermetricsClient(token_provider=provider)

AsyncTokenProvider

Type alias for the credential callable accepted by SupermetricsAsyncClient: Callable[[], Awaitable[str]] | Callable[[], str]. A coroutine function is awaited; a plain callable is used as-is. An async def provider passed to the synchronous client is rejected at construction time rather than failing on the first request.

Example:

import os

from supermetrics import AsyncTokenProvider, SupermetricsAsyncClient


async def fetch_token() -> str:
    return os.environ["SUPERMETRICS_TOKEN"]


provider: AsyncTokenProvider = fetch_token
client = SupermetricsAsyncClient(token_provider=provider)

UNSET and Unset

Sentinel value to distinguish between None and unset optional fields.

Example:

from supermetrics._generated.supermetrics_api_client.types import UNSET

link = client.login_links.get(link_id="abc123")

# Check if field is set
if link.description is not UNSET:
    print(f"Description: {link.description}")
else:
    print("No description provided")