July 3, 2026

Auto-Routing API Clients by Key Prefix

The Syndicate Links API has two distinct surfaces. Merchants manage programs, products, partnerships and webhooks. Affiliates and AI agents manage tracking links, conversions, balances and payouts. There is essentiall...

Auto-Routing API Clients by Key Prefix

Auto-Routing API Clients by Key Prefix

The Syndicate Links API has two distinct surfaces. Merchants manage programs, products, partnerships and webhooks. Affiliates and AI agents manage tracking links, conversions, balances and payouts. There is essentially no overlap between the two — a merchant key cannot call an affiliate endpoint and the reverse is also a 403.

The naive SDK shape is to make the user pick:

# bad
from syndicate_links import MerchantClient, AffiliateClient

client = MerchantClient(api_key="mk_live_…")
# or
client = AffiliateClient(api_key="ak_live_…")

This is fine until someone passes a merchant key into AffiliateClient and gets a confusing 403 on their first call. Or until they are writing a script that has both kinds of keys in environment variables and now needs a if api_key.startswith("mk_"): MerchantClient(...) else: AffiliateClient(...) block at the top of every file. The user is doing work the SDK should do for them.

So the Python SDK has one client. You give it a key. It figures out which surface you wanted.

from syndicate_links import SyndicateClient

client = SyndicateClient(api_key="mk_live_abc…")
client.merchant.programs.list()        # works
client.affiliate.balance.get()         # raises ForbiddenError immediately

client = SyndicateClient(api_key="aff_agent_xyz…")
client.affiliate.balance.get()         # works
client.merchant.programs.list()        # raises ForbiddenError immediately

The implementation is twelve lines. Two prefix tuples and a function:

MERCHANT_PREFIXES = ("mk_live_", "mk_test_")
AFFILIATE_PREFIXES = ("ak_live_", "ak_test_", "aff_agent_", "aff_human_")


def _detect_key_type(api_key: str) -> str:
    if any(api_key.startswith(p) for p in MERCHANT_PREFIXES):
        return "merchant"
    if any(api_key.startswith(p) for p in AFFILIATE_PREFIXES):
        return "affiliate"
    return "unknown"

Detection runs once in _BaseClient.__init__ and the result is stashed on self.key_type. The two sub-clients are then gated behind properties that check the type and raise locally instead of letting the request go out:

@property
def merchant(self) -> "MerchantClient":
    if self.key_type == "affiliate":
        raise ForbiddenError(
            403,
            "Merchant methods require a merchant key (mk_live_...), "
            "but an affiliate key was provided.",
        )
    return self._merchant

@property
def affiliate(self) -> "AffiliateClient":
    if self.key_type == "merchant":
        raise ForbiddenError(
            403,
            "Affiliate methods require an affiliate key (ak_live_... or aff_agent_...), "
            "but a merchant key was provided.",
        )
    return self._affiliate

Two things matter here that are easy to miss.

The error fires on attribute access, not on the HTTP call. The traceback points at client.merchant on the line where the user actually wrote it, not at some buried request() call inside the sub-client. The user sees their own code in the trace.

The error message tells you which prefix you needed. Not "403 Forbidden." Not "invalid credentials." It tells you "you gave me an affiliate key and asked for a merchant method, here are the prefixes that would work." Most 403s in most SDKs are unsigned post-it notes. Ours name the problem.

The unknown branch is intentional — if a future key type ships (we already reserved aff_human_ for human affiliates and there will be more), the SDK does not crash on import. It just assumes the user knows what they are doing and lets the API return the real error. We would rather degrade than block a working integration on an SDK update lag.

This is the kind of detail an OpenAPI generator will never produce. Generators emit one client per tag, or one giant client with every method on it, and leave the routing problem to the user. They do not know your key format, they do not know that merchant and affiliate are mutually exclusive surfaces, and they cannot give you a useful error message because they do not know what is useful in your domain. A first-party SDKs can.

Small details like this are why first-party SDKs beat generated ones from OpenAPI.