vercel-connect
Python SDK for Vercel Connect, a credential broker for third-party APIs.
You exchange your deployment's token for a short-lived credential for an upstream service. Your project never stores provider secrets, and Connect owns the OAuth client, PKCE, refresh, and revocation server-side.
pip install vercel-connect
Usage
import httpx
from vercel.connect import ConnectAppTokenSubject, get_token
token = await get_token("github/my-app", subject=ConnectAppTokenSubject())
async with httpx.AsyncClient() as client:
await client.get(
"https://api.github.com/user/repos",
headers={"Authorization": f"Bearer {token}"},
)
The same surface is available synchronously, with identical names and arguments:
from vercel.connect.sync import ConnectAppTokenSubject, get_token
token = get_token("github/my-app", subject=ConnectAppTokenSubject())
Use a plain with block and vercel.connect.sync together; mixing an async call
into a sync session, or the reverse, is rejected.
Subjects
Whose authority the credential carries:
| Subject | Authority | Needs |
|---|---|---|
ConnectAppTokenSubject() |
The integration itself | An installation |
ConnectUserTokenSubject(id=...) |
One named end user | That user's consent |
ConnectJwtBearerTokenSubject(sub=...) |
A user asserted by your app | Pre-established trust |
ConnectTokenExchangeSubject(token=...) |
A credential you already hold | The inbound token |
app is one shared credential per installation: simple, always available, but
ambient authority. user preserves the provider's own permission model per
person and names them in the provider's audit log, at the cost of a consent flow.
Subjects are typed values rather than plain strings because three of the four
carry their own fields, so subject="user" could not say which user:
ConnectUserTokenSubject(id="u_123", issuer="https://idp.example.com")
ConnectJwtBearerTokenSubject(sub="u_123", additional_claims={"tenant": "acme"})
Value types
Every type on this surface is a frozen Pydantic model, so you get validation on
construction, autocompletion, exact match/case narrowing, model_dump() for
logging, and immutability, which means a subject cannot be mutated after a
credential has been cached against it:
detail = ConnectGitHubAppInstallationAuthorizationDetail(permissions=["contents:read"])
detail.model_dump() # {'org': None, 'permissions': ('contents:read',), ...}
detail.permissions = ["admin"] # ConnectValidationError: frozen
Construction is by keyword, a misspelled field is an error rather than a silently
dropped value, and every rejection raises ConnectValidationError, so you never
need to catch Pydantic's own error type. Containers of strings accept any
container and store a tuple; a bare string is rejected rather than expanded into
one entry per character:
get_token(..., scopes="repo:read") # ConnectValidationError, and a type error
get_token(..., scopes=["repo:read"]) # correct
Authorization as control flow
The two "required" errors are not bugs, they are states with a remedy:
from vercel.connect import (
ConnectUserTokenSubject,
UserAuthorizationRequiredError,
get_token,
start_authorization,
)
subject = ConnectUserTokenSubject(id=user_id)
try:
token = await get_token("linear/my-app", subject=subject)
except UserAuthorizationRequiredError:
authorization = await start_authorization(
"linear/my-app", subject=subject, return_url="https://myapp.com/cb"
)
return redirect(authorization.url)
A CLI or headless process has nowhere to redirect to, so it asks for a device code and polls. Each outcome is its own error, so the loop never inspects an error code:
import anyio
from vercel.connect import (
AuthorizationDeniedError,
AuthorizationExpiredError,
AuthorizationPendingError,
ConnectOptions,
)
authorization = await start_authorization(
"linear/my-app", subject=subject, device_code=True
)
print(f"Enter {authorization.device_code} at {authorization.url}")
while True:
try:
token = await get_token(
"linear/my-app", subject=subject, options=ConnectOptions(force_refresh=True)
)
break
except AuthorizationPendingError:
await anyio.sleep(5)
except (AuthorizationDeniedError, AuthorizationExpiredError):
raise # terminal: nothing to wait for
force_refresh=True is what makes the poll reach the server; without it a cached
credential would be returned. slow_down is reported as
AuthorizationPendingError too, so a fixed interval stays correct.
Inbound triggers
A connector with triggers enabled forwards provider webhooks to your project with a Vercel OIDC token attached, so you verify one thing instead of a different signature scheme per provider:
from vercel.connect import verify_connect_webhook
claims = await verify_connect_webhook(request.headers)
Verification pins the issuer to Vercel's OIDC service, accepting both
https://oidc.vercel.com and the team-scoped https://oidc.vercel.com/<team>,
allows only RS256, and fails closed when the expected project and environment cannot be resolved. It
accepts any valid Vercel OIDC token for this project and environment; it is not
pinned to a specific connector or deployment.
Configuration
To configure advanced options, use a session context manager, and pass
ConnectServiceOptions:
from vercel.api import session
from vercel.connect import ConnectAppTokenSubject, ConnectServiceOptions, get_token
async with session(
service_options=[ConnectServiceOptions(base_url="https://staging.example.com")]
):
token = await get_token("github/my-app", subject=ConnectAppTokenSubject())
Local development
On Vercel the OIDC token is injected automatically. Locally:
vercel link
vercel env pull # writes VERCEL_OIDC_TOKEN into .env.local
The connector must be attached to your project and enabled for the target environment, or every call fails.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vercel_connect-0.1.0.tar.gz.
File metadata
- Download URL: vercel_connect-0.1.0.tar.gz
- Upload date:
- Size: 32.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2875a92e0b08af335f8c11df7e32c824ba6fd7078710f11699e3a49f6456482
|
|
| MD5 |
e77893ba9914f90b1cf6a9fff5cfd47f
|
|
| BLAKE2b-256 |
206892f87729e98898d1404fc29ab5c47c49b6ba0beef36a22bc309bbe2ed3d4
|
File details
Details for the file vercel_connect-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vercel_connect-0.1.0-py3-none-any.whl
- Upload date:
- Size: 41.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af44bd54bfd1809baf382abd6915301ec12ad5956a580601f43f7af0ebcf559a
|
|
| MD5 |
fe8c6a2821c3e92204993ebd5c3afe3e
|
|
| BLAKE2b-256 |
58bf81eb0715b371d9af51a8ae3d8fdb067387bf4e829f5f7fa9bc8b2874261e
|