VendoVendo Docs
Concepts

Errors

Typed error classes mapped to the `Vendo-Error-Code` header.

All SDK errors inherit from a common base (VendoError in Python/TS, VendoError enum in Swift). Every error carries enough context to act on it without string parsing.

Error code reference

The Vendo-Error-Code response header is the canonical wire-format code for proxy-returned errors. Several codes can map to the same SDK class — binding_missing and connection_revoked both raise NotConnected, for example. Two codes (vendo_only_feature, identity_not_present) are SDK-only: the SDK raises them before any HTTP call and they never appear on the wire.

CodePython / TS classSwift caseOriginWhen it's raised
app_unknownAuthError.authproxyThe presented vendo_sk_* bearer was not found
app_revokedAuthError.authproxyThe app key was revoked by the tenant
app_expiredAuthError.authproxyThe app key passed its expiry
binding_missingNotConnected.notConnectedproxyNo connection is bound for the requested provider
connection_revokedNotConnected.notConnectedproxyThe bound connection was revoked
connection_needs_reauthNeedsReauth.needsReauthproxyThe bound OAuth token expired; user must re-authorize
balance_exhaustedBalanceExhausted.balanceExhaustedproxyThe tenant's wallet hit zero
spend_cap_dailySpendCapExceeded.spendCapExceededproxyThe per-app daily spend cap fired
spend_cap_monthlySpendCapExceeded.spendCapExceededproxyThe per-app monthly spend cap fired
upstream_rate_limitedRateLimited.rateLimitedproxyThe upstream provider returned 429
upstream_errorUpstreamError.upstreamproxyThe upstream provider returned a non-2xx, non-429 response
validation_failedValidationError.validationproxyRequest payload or webhook signature was invalid
idempotency_conflictIdempotencyConflict.idempotencyConflictproxySame idempotency key with a different payload hash
vendo_only_featureVendoOnlyFeature.vendoOnlyFeatureSDKOSS-mode call hit a surface that requires VENDO_API_KEY
identity_not_presentIdentityNotPresent.identityNotPresentSDKforRequest called without an X-Vendo-User-JWT header

Codes the SDK does not recognise raise the base VendoError with the code preserved on err.code.

Catching errors

All error classes live in vendo.errors. They all inherit from vendo.errors.VendoError.

import vendo
from vendo.errors import (
    NotConnected,
    NeedsReauth,
    AuthError,
    RateLimited,
    BalanceExhausted,
    SpendCapExceeded,
    VendoOnlyFeature,
    VendoError,
)

try:
    tok = vendo.token("slack")
except NotConnected as e:
    # binding_missing or connection_revoked
    url = vendo.connect_url(e.slug, return_to="https://yourapp.com/after-connect")
    print(f"Connect Slack first: {url}")
except NeedsReauth as e:
    # connection_needs_reauth — user must re-authorize
    print(f"Re-authorize: {e.connect_url}")
except AuthError:
    # app_unknown / app_revoked / app_expired
    print("Invalid or revoked VENDO_API_KEY.")
except RateLimited as e:
    # upstream_rate_limited
    print(f"Rate limited — retry after {e.retry_after}s")
except BalanceExhausted:
    print("Wallet empty — top up at vendo.run")
except SpendCapExceeded as e:
    # spend_cap_daily or spend_cap_monthly — inspect e.code
    print(f"Spend cap hit ({e.code}); retry after {e.retry_after}s")
except VendoOnlyFeature as e:
    print(f"Not available in OSS mode: {e}")
except VendoError as e:
    print(f"Unexpected SDK error [{e.code}]: {e}")

Useful attributes on VendoError:

AttributeTypeDescription
codestrCanonical Vendo-Error-Code value
statusint | NoneHTTP status from the response
slugstr | NoneProvider slug (NotConnected, NeedsReauth)
connect_urlstr | NoneOAuth popup URL to fix the issue
retry_afterint | NoneSeconds to wait before retrying (RateLimited, SpendCapExceeded)
suggested_fixstr | NoneOptional human-readable remediation hint
messagestrHuman-readable description

All error classes are named exports from @vendodev/sdk.

import {
  Vendo,
  NotConnected,
  NeedsReauth,
  AuthError,
  RateLimited,
  BalanceExhausted,
  SpendCapExceeded,
  VendoOnlyFeature,
  VendoError,
} from "@vendodev/sdk";

const vendo = new Vendo();

try {
  const tok = await vendo.token("slack");
} catch (e) {
  if (e instanceof NotConnected) {
    console.log("Connect first:", e.connectUrl);
  } else if (e instanceof NeedsReauth) {
    console.log("Re-authorize:", e.connectUrl);
  } else if (e instanceof AuthError) {
    console.log("Invalid VENDO_API_KEY");
  } else if (e instanceof RateLimited) {
    console.log(`Retry after ${e.retryAfter}s`);
  } else if (e instanceof BalanceExhausted) {
    console.log("Wallet empty");
  } else if (e instanceof SpendCapExceeded) {
    console.log(`Spend cap hit (${e.code})`);
  } else if (e instanceof VendoOnlyFeature) {
    console.log("Set VENDO_API_KEY to use this feature");
  } else if (e instanceof VendoError) {
    console.log(`SDK error [${e.code}]: ${e.message}`);
  } else {
    throw e;
  }
}

Useful properties on VendoError:

PropertyTypeDescription
codestringCanonical Vendo-Error-Code value
statusnumber | undefinedHTTP status
slugstring | undefinedProvider slug
connectUrlstring | undefinedOAuth popup URL
retryAfternumber | undefinedSeconds to retry
messagestringHuman-readable description

All errors are cases of the VendoError enum. Use catch pattern matching.

import Vendo

do {
    let token = try await vendo.token("openai")
} catch VendoError.notConnected(let slug, let message) {
    print("Connect \(slug) first: \(message)")
} catch VendoError.needsReauth(let slug, let message, let connectURL) {
    print("Re-authorize \(slug): \(connectURL?.absoluteString ?? "")")
} catch VendoError.rateLimited(let retryAfter) {
    print("Rate limited — retry in \(retryAfter ?? 60)s")
} catch VendoError.balanceExhausted(let message) {
    print("Wallet empty: \(message)")
} catch VendoError.spendCapExceeded(let message) {
    print("Spend cap hit: \(message)")
} catch VendoError.vendoOnlyFeature(let message) {
    print("Vendo-only: \(message)")
} catch VendoError.auth(let message) {
    print("Auth error: \(message)")
} catch let e as VendoError {
    print("SDK error: \(e.localizedDescription)")
}

Testing with mocks

All three SDKs provide a MockClient that can be configured to throw specific errors:

from vendo.testing import MockClient, fake_connection
from vendo.errors import NotConnected
import pytest

mock = MockClient.with_connections([
    fake_connection(slug="slack", status="connected",
                    credential={"access_token": "xoxb-fake"}),
])

assert mock.token("slack") == "xoxb-fake"

with pytest.raises(NotConnected):
    mock.token("missing")
import { MockClient, fakeConnection } from "@vendodev/sdk";

const mock = MockClient.withConnections([
  fakeConnection({ slug: "slack", credential: { access_token: "xoxb-fake" } }),
]);

expect(await mock.token("slack")).toBe("xoxb-fake");
await expect(mock.token("missing")).rejects.toBeInstanceOf(NotConnected);
// Use environment variables pointing at vendo dev server
// or inject a custom URLSession mock
let mockToken = "fake-token"
setenv("VENDO_TOKEN_OPENAI", mockToken, 1)

let vendo = try Vendo(apiKey: "byok")
let token = try await vendo.token("openai")
assert(token == mockToken)

On this page