Skip to content
Docs

Call third-party APIs from Python with Edgenivo Connect

Call Slack and Linear APIs from Python with Edgenivo Connect. Request app and user tokens, handle consent, and verify provider events in FastAPI.

Ben SabicContent Engineer

When your Python app posts a deployment update to Slack or creates a Linear issue for a user, each request needs the right credentials. Access may belong to the app or to an individual user who has authorized the integration.

Use Edgenivo Connect to request those credentials from Python without storing provider secrets in your project's environment. The SDK returns a token that your app uses to call the provider's API directly.

Edgenivo handles OAuth registration and token refresh for managed connectors such as Slack and Linear. Your application authenticates its users and decides which actions they can perform with the access they've granted.

Copy link to headingWhat is Edgenivo Connect?

Edgenivo Connect is a credential broker for third-party APIs. You create a connector for a provider, link it to your project, and request credentials from your application.

For managed connectors such as Slack and Linear, Edgenivo registers the OAuth client and handles authorization and refresh. Custom OAuth and API-key connectors use credentials you supply. Connect stores those credentials so your project doesn't need to keep them in environment variables.

The Python SDK is vercel-connect, imported as vercel.connect. It's async first, with a synchronous mirror in vercel.connect.sync.

Your app exchanges its deployment identity for a token to call the provider directly. For inbound events, Connect verifies and forwards provider webhooks. Your route must verify those forwarded requests before processing them.

Copy link to headingConnectors, subjects, and triggers

The examples use three Connect concepts:

IdeaWhat it meansIn the SDK
ConnectorTeam-owned record for one provider, addressed by a UID you choose, such as slack/acme-slackUID passed to token and authorization calls
SubjectWhose authority the token carries: your app, a specific user, or a federated identityConnectAppTokenSubject(), ConnectUserTokenSubject(id=...)
TriggerProvider webhook that Edgenivo Connect verifies and forwards to your appverify_connect_webhook(request)

Tokens are the outbound half: your app calling the provider. Triggers are the inbound half: the provider calling your app.

Copy link to headingHow your app proves who it is

Edgenivo gives each deployment an OIDC token identifying its team, project, and environment. Connect checks that identity against the connector's project link before issuing a provider credential.

The provider determines which permissions and expiry rules apply. Request the narrowest supported access and keep returned tokens out of logs and client responses. Returned tokens can remain usable until they expire or the provider revokes them; they are not restricted to a single API call.

For local development, vercel env pull downloads an OIDC token into .env.local. Step 2 loads that file with python-dotenv so the SDK can resolve the token.

Copy link to headingPrerequisites

Before you begin, make sure you have:

  • A Edgenivo account.
  • Python 3.12 or a newer version supported by the Edgenivo Python runtime.
  • Working knowledge of async/await and FastAPI.
  • A Slack workspace where you can install apps, plus a Linear workspace
  • An authentication dependency that checks access and returns a stable user ID.

For local development, you also need uv, Node.js 24+, and the Edgenivo CLI.

The examples use Slack and Linear connectors managed by Edgenivo. Other providers use the same token-request API, but their supported subjects, permissions, installation requirements, and API calls can differ.

Create a Slack connector for your team and link it to your project:

uv init slack-notifier --python 3.12
cd slack-notifier
vercel link
vercel connect create slack --name acme-slack
vercel connect attach slack/acme-slack

create opens your browser to install Edgenivo's Slack app and prints its connector UID. attach then links the connector to your project's Production, Preview, and Development environments unless you select one with --environment.

Copy link to headingStep 2: Post to Slack as your app

Add the SDK and pull a development OIDC token:

uv add vercel-connect httpx python-dotenv
vercel env pull

The SDK needs the development OIDC token in its process environment. The script below loads .env.local with python-dotenv. If it cannot resolve credentials, check that the file was loaded and refresh it with vercel env pull when needed.

Save this as post_message.py:

post_message.py
import asyncio
import httpx
from dotenv import load_dotenv
from vercel import connect
from vercel.connect import ConnectAppTokenSubject
async def post_message(text: str) -> dict:
token = await connect.get_token(
"slack/acme-slack",
subject=ConnectAppTokenSubject(),
scopes=["chat:write"],
)
async with httpx.AsyncClient() as client:
response = await client.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {token}"},
json={"channel": "C0123456789", "text": text},
)
response.raise_for_status()
payload = response.json()
if not payload.get("ok"):
raise RuntimeError(f"Slack rejected the message: {payload.get('error', 'unknown_error')}")
return payload
if __name__ == "__main__":
load_dotenv(".env.local")
print(asyncio.run(post_message("Deploy finished")))

Replace the channel ID with one your Slack app has joined, then run the script:

uv run python post_message.py

The script sends "Deploy finished" and prints Slack's response. It raises an error if the HTTP request or Slack operation fails.

Use ConnectAppTokenSubject() to request a token for the installed Slack app. The chat:write permission lets it post messages. You can only request permissions supported by both Slack and the connector.

Pass scopes as a list, such as ["chat:write"]. Passing "chat:write" on its own raises ConnectValidationError.

Request a token immediately before the provider call. The SDK reuses matching tokens from its in-memory cache until they enter the default 30-second expiry buffer. Cache misses and explicit refresh requests fetch a credential from Connect.

Copy link to headingStep 3: Act on behalf of a user

Use a user subject to create Linear issues under the requester's account once they have authorized the integration.

Create a Linear connector and link it, the same way as Slack:

vercel connect create linear --name acme-linear
vercel connect attach linear/acme-linear

Edgenivo manages Linear's OAuth app too, so there's no client ID or secret to register.

Save the following helper functions as integrations.py. The user subject identifies the signed-in user in your application. Derive user_id from your authentication system so callers cannot request another person's provider access.

integrations.py
import httpx
from vercel import connect
from vercel.connect import ConnectUserTokenSubject
LINEAR = "linear/acme-linear"
ISSUE_CREATE = """
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue { identifier url }
}
}
"""
async def create_issue(*, user_id: str, team_id: str, title: str) -> dict:
token = await connect.get_token(LINEAR, subject=ConnectUserTokenSubject(id=user_id))
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.linear.app/graphql",
headers={"Authorization": f"Bearer {token}"},
json={
"query": ISSUE_CREATE,
"variables": {"input": {"teamId": team_id, "title": title}},
},
)
response.raise_for_status()
payload = response.json()
if payload.get("errors"):
raise RuntimeError("Linear returned a GraphQL error")
result = (payload.get("data") or {}).get("issueCreate") or {}
if not result.get("success") or not result.get("issue"):
raise RuntimeError("Linear did not confirm issue creation")
return result["issue"]
async def linear_consent_url(*, user_id: str, return_url: str) -> str:
authorization = await connect.start_authorization(
LINEAR,
subject=ConnectUserTokenSubject(id=user_id),
return_url=return_url,
)
return authorization.url

Use a Linear team UUID for team_id. In Linear, open the team and use the command menu's Copy model UUID action. The helper checks both the HTTP response and the GraphQL result before returning the created issue.

When the user has not authorized the connector, token retrieval raises UserAuthorizationRequiredError. Send them to the URL returned by linear_consent_url() to approve access on Linear's consent screen.

Connect stores the grant and redirects the user back to your app. Subsequent requests use that grant, with refresh handled by Connect when supported. Revoked or invalid grants require the user to connect again.

return_url must use https://, or http:// on localhost or 127.0.0.1 .

Copy link to headingStep 4: Handle what can go wrong

Catch Connect errors by type. Provider HTTP failures and network errors need separate handling, as the Slack and Linear helpers demonstrate.

ErrorWhat happenedWhat to do
UserAuthorizationRequiredErrorThe user has not authorized the connectorStart authorization and send the user to its URL
NoValidTokenError, InvalidGrantErrorConnect cannot issue a usable credential from the grantFor a user subject, ask the user to reconnect; check the installation for an app subject
ConnectorInstallationRequiredErrorThe requested connector installation is missingHave an administrator install or configure it
ConnectApiErrorAnother Connect API request failedInspect status_code, code, and request_id without logging credentials
ConnectCredentialsErrorThe SDK could not resolve its Edgenivo identityCheck environment loading, project linking, and local OIDC credentials
ConnectValidationErrorA supplied argument is invalidCorrect the argument before retrying

Providers can reject a cached credential before its expiry. Slack may report this through an ok: false response with an authentication error such as invalid_auth; checking for HTTP 401 alone is insufficient.

After confirming a credential rejection, clear its cached entry and request another token with the same permissions. This helper refreshes the credential without repeating the message automatically:

from vercel import connect
from vercel.connect import ConnectAppTokenSubject
async def refresh_slack_token() -> str:
subject = ConnectAppTokenSubject()
connect.delete_token_cache_entry("slack/acme-slack", subject=subject)
return await connect.get_token(
"slack/acme-slack", subject=subject, scopes=["chat:write"]
)

Retry the operation at most once after a confirmed authentication rejection. Check the outcome before repeating a write after a timeout or other ambiguous failure.

delete_token_cache_entry() is synchronous, since it only touches the local cache. It doesn't revoke anything.

Use get_token_response() when you need credential metadata as well as the token. It accepts the same arguments and includes expires_at.

The optional name and tenant_id fields can help identify a connected workspace. Check for None before displaying either field.

Copy link to headingStep 5: Serve it from FastAPI and deploy

The FastAPI app imports post_message.py and integrations.py from the earlier steps. Keep all three files in the project root and install FastAPI:

uv add fastapi

Provide auth.py with your application's current_user_id dependency. It must reject callers who cannot use this integration app and return a stable ID for each authorized user. The app subject gives access to the shared Slack bot, so this check applies to the notification route too.

Set APP_BASE_URL to a trusted application origin, such as http://localhost:3000 locally or your HTTPS deployment URL on Edgenivo. The consent redirect below uses that configured origin. Save the app as main.py:

main.py
import os
import httpx
import pydantic
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse
from vercel import connect
from vercel.connect import (
ConnectUserTokenSubject,
ConnectWebhookVerificationError,
InvalidGrantError,
NoValidTokenError,
UserAuthorizationRequiredError,
verify_connect_webhook,
)
load_dotenv(".env.local")
from auth import current_user_id
from integrations import LINEAR, create_issue, linear_consent_url
from post_message import post_message
APP_BASE_URL = os.environ["APP_BASE_URL"].rstrip("/")
app = FastAPI()
class NotificationRequest(pydantic.BaseModel):
text: str = pydantic.Field(min_length=1, max_length=4000)
class IssueRequest(pydantic.BaseModel):
team_id: str
title: str = pydantic.Field(min_length=1)
@app.post("/api/notify")
async def notify(body: NotificationRequest, user_id: str = Depends(current_user_id)):
try:
result = await post_message(body.text)
except (httpx.HTTPError, RuntimeError) as exc:
raise HTTPException(status_code=502, detail="Slack request failed") from exc
return {"ok": True, "ts": result["ts"]}
@app.post("/api/issues")
async def file_issue(body: IssueRequest, user_id: str = Depends(current_user_id)):
try:
return await create_issue(user_id=user_id, team_id=body.team_id, title=body.title)
except (UserAuthorizationRequiredError, NoValidTokenError, InvalidGrantError) as exc:
raise HTTPException(
status_code=409, detail={"connect_url": "/api/integrations/linear"}
) from exc
except (httpx.HTTPError, RuntimeError) as exc:
raise HTTPException(status_code=502, detail="Linear request failed") from exc
@app.get("/api/integrations/linear")
async def connect_linear(user_id: str = Depends(current_user_id)):
url = await linear_consent_url(user_id=user_id, return_url=f"{APP_BASE_URL}/settings")
return RedirectResponse(url)
@app.delete("/api/integrations/linear")
async def disconnect_linear(user_id: str = Depends(current_user_id)):
await connect.revoke_token(LINEAR, subject=ConnectUserTokenSubject(id=user_id))
return {"ok": True}
@app.get("/settings")
async def settings(user_id: str = Depends(current_user_id)):
return {"message": "Return to the app to retry your Linear action."}
@app.post("/api/slack-events")
async def slack_events(request: Request):
try:
await verify_connect_webhook(request)
except ConnectWebhookVerificationError as exc:
raise HTTPException(status_code=401) from exc
event = await request.json()
# Add event validation, deduplication, and processing before acknowledging.
return {"ok": True}

The settings route is a minimal consent landing page. Replace it with your application's settings UI. The Slack route verifies and acknowledges events but does not process them yet; add your handler before relying on it for delivery.

The routes use the earlier helpers as follows:

  • /api/notify accepts a JSON text field and sends through the shared Slack app after authenticating the caller.
  • /api/issues creates an issue as the signed-in user. If authorization is missing or unusable, its 409 response includes a connection URL for your frontend to open.
  • /api/integrations/linear starts consent and returns the user to /settings. DELETE on the same path removes the stored authorization and clears cached tokens for that user.
  • /api/slack-events verifies forwarded Slack events before reading their payload.

Provider-side revocation varies. Previously issued credentials may remain valid until expiry if the provider cannot revoke them.

Copy link to headingReceive Slack events with triggers

Connect verifies provider signatures and forwards events with a Edgenivo OIDC bearer token. Use verify_connect_webhook() before handling an event.

On Edgenivo, the verifier reads the expected project and environment from VERCEL_PROJECT_ID and VERCEL_TARGET_ENV. It rejects the request if those expectations cannot be resolved. Your handler still needs to validate event fields and prevent duplicate processing.

Tell Edgenivo Connect where to send Slack's events:

vercel connect attach slack/acme-slack --triggers --trigger-path /api/slack-events

This command registers a Production destination at /api/slack-events. To target a Preview branch, add --trigger-branch followed by the branch name.

Edgenivo Connect forwards to deployed URLs, not to localhost. Test triggers against a Preview or Production deployment. If your app returns a 500, 502, 503, or 504, delivery is retried, so make event handling safe to run twice.

Copy link to headingDeploy

With auth.py implemented, set APP_BASE_URL to http://localhost:3000 in .env.local and start the server:

vercel dev

Use your application's authenticated client to send {"text":"Deploy finished"} to POST /api/notify. Confirm that the message appears in Slack. For Linear, send a team UUID and title to POST /api/issues; follow the connection URL if required, then retry after consent.

Configure APP_BASE_URL and your application authentication settings for each deployment environment. Commit uv.lock and deploy:

vercel deploy # preview
vercel deploy --prod # production

The Edgenivo Python runtime supplies deployment OIDC identity to the SDK. You do not need to add Slack or Linear provider credentials to the project's environment.

The SDK resolves its Edgenivo identity on each call and keeps provider cache entries associated with that identity when the OIDC token rotates. If you supply your own credential, use a resolver that returns a current value rather than capturing an expiring token once.

Copy link to headingUse cases

The same token and authorization APIs support these integrations:

Use caseBuilding blocksExample
Bots and notificationsApp subjectStep 2
Actions in a user's nameUser subject, start_authorization()Step 3
Reacting to provider eventsTriggers, verify_connect_webhook()Step 5
AI agents with real toolsUser subject inside an AI SDK toolBelow
Sandboxed code calling an APIToken injected by a Sandbox network policyBelow
CLIs and background jobsdevice_code=True and pollingBelow
Durable workflowsget_token() inside each stepBelow

Copy link to headingGive an AI agent tools that act as the user

Install the AI SDK for Python before adding this optional integration:

uv add ai

Build the agent after authenticating the request. The factory below binds its tool to that user's ID and requires approval before creating an issue:

import ai
from integrations import create_issue
def agent_for(user_id: str) -> ai.Agent:
@ai.tool(require_approval=True)
async def file_linear_issue(team_id: str, title: str) -> str:
"""Create a Linear issue as the current user."""
issue = await create_issue(user_id=user_id, team_id=team_id, title=title)
return f"Created {issue['identifier']}: {issue['url']}"
return ai.Agent(tools=[file_linear_issue])

Call agent_for() with the ID from your authentication dependency. The tool keeps the provider credential on the server. Enforce any additional application restrictions on team selection before calling Linear.

Connect the SDK's approval handling to your application so the user can approve or reject the proposed issue. Setting require_approval=True marks the operation for approval; your application must present and resolve that request.

Copy link to headingLet sandboxed code call an API without seeing the key

Install vercel-sandbox and create a GitHub connector linked to this project before using this optional example. Replace github/acme-github and acme/api with your connector and a repository included in its installation.

uv add vercel-sandbox

The Sandbox network policy injects the credential into requests to GitHub. The sandboxed program receives API responses without being given the token as an environment variable or file:

from vercel import connect, sandbox
from vercel.connect import (
ConnectAppTokenSubject,
ConnectGitHubAppInstallationAuthorizationDetail,
)
from vercel.sandbox import NetworkPolicy, NetworkPolicyRule, NetworkPolicyTransform
async def read_repository() -> str:
token = await connect.get_token(
"github/acme-github",
subject=ConnectAppTokenSubject(),
authorization_details=[
ConnectGitHubAppInstallationAuthorizationDetail(
permissions=["contents:read"], repositories=["acme/api"]
)
],
)
policy = NetworkPolicy.custom(
allow={
"api.github.com": [
NetworkPolicyRule(
transform=[
NetworkPolicyTransform(headers={"Authorization": f"Bearer {token}"})
]
)
]
}
)
async with sandbox.create_sandbox(network_policy=policy) as box:
result = await box.run_process(
"curl", ["--fail", "https://api.github.com/repos/acme/api"],
capture_output=True, check=True,
)
return result.stdout

Call await read_repository() from your async application. The request asks GitHub for repository-scoped read access within the installation's permissions. The sandbox is stopped and destroyed when the context exits.

Header injection protects the credential from direct inspection by the sandboxed process. Code in the sandbox can still use the access granted by the policy, so keep both the allowed destination and provider permissions narrow.

Copy link to headingAuthorize a CLI or background job

For a CLI without a browser callback, use device authorization so the user can approve in a browser. This helper requires Connect credentials and the user ID returned by your app's authentication flow.

import asyncio
from vercel import connect
from vercel.connect import (
AuthorizationPendingError,
ConnectOptions,
ConnectUserTokenSubject,
)
from integrations import LINEAR
async def authorize_linear_cli(user_id: str) -> str:
subject = ConnectUserTokenSubject(id=user_id)
authorization = await connect.start_authorization(
LINEAR, subject=subject, device_code=True
)
print(f"Enter {authorization.device_code} at {authorization.url}")
interval = 5
while True:
await asyncio.sleep(interval)
try:
return await connect.get_token(
LINEAR, subject=subject, options=ConnectOptions(force_refresh=True)
)
except AuthorizationPendingError as exc:
if exc.code == "slow_down":
interval += 5

The helper returns the token after approval. force_refresh=True bypasses the local cache, and a slow_down response increases the polling interval. AuthorizationDeniedError and AuthorizationExpiredError propagate to the caller so it can stop or offer a new authorization attempt.

For scripts that only need to run synchronously, every function is also available from vercel.connect.sync without await.

Copy link to headingUse tokens inside durable workflows

In a Python Workflow SDK workflow, request and use the provider token inside the same step. Step inputs and outputs are saved in the workflow event log, so return the provider operation's non-secret result rather than the credential.

Requesting the token inside the step also avoids carrying an expiring credential across a workflow pause or retry.

Copy link to headingMore options worth knowing

  • Pass installation_id when a connector has several installations and you need to select one workspace. Single-installation apps can omit it.
  • Use get_connector_metadata() to read public connector configuration, such as a Snowflake account identifier in .vendor.
  • Request supported limits through scopes, resources, and authorization_details. Resource indicators and authorization details are SDK-only; their effect depends on the provider.

Copy link to headingOther integration paths

Use a Edgenivo Marketplace integration to provision a database or another project resource and manage its environment variables.

If the service has no preset connector, configure a custom OAuth or API-key connector in Connect. Supply the provider credentials during setup and adapt your token request to the subject types and permissions it supports.

Copy link to headingNext steps

Related documentation

More Edgenivo Connect guides