Connect any OAuth 2.0 service
Connect services without a preset, such as Okta, Shopify, Salesforce, HubSpot or your own, by giving their sign-in URLs. Domains, tenants and stores come from secrets or from the person connecting.
Presets are shortcuts, not a limit. Any service that uses OAuth 2.0's standard sign-in (the authorization code flow) works by giving its URLs. No SDK update is needed.
The basics
Find the service's authorization URL and token URL in its developer docs, create an OAuth app there with the redirect URI https://callables.app/connections/callback, and declare:
from callables import Connection, Secret
crm = Connection(
"crm",
authorize_url="https://login.somecrm.com/oauth/authorize",
token_url="https://login.somecrm.com/oauth/token",
scopes=["contacts.read"],
client_id=Secret("CRM_CLIENT_ID"),
client_secret=Secret("CRM_CLIENT_SECRET"),
)Set CRM_CLIENT_ID and CRM_CLIENT_SECRET as project secrets, and in .env to test with call connect.
When the URLs differ per company or per person
Put a {placeholder} in the URL, and fill it from values:
Secret("NAME")for a value that's the same for everyone in the project, such as your company's Okta domain. It's a project secret on Callables, and an environment variable locally.Ask("prompt")for a value each person types when they connect, such as their own Shopify store.
A service on your company's own domain:
okta = Connection(
"okta",
authorize_url="https://{domain}/oauth2/v1/authorize",
token_url="https://{domain}/oauth2/v1/token",
scopes=["openid", "email"],
values={"domain": Secret("OKTA_DOMAIN")},
)A service where each person has their own instance:
from callables import Ask
shopify = Connection(
"shopify",
authorize_url="https://{shop}.myshopify.com/admin/oauth/authorize",
token_url="https://{shop}.myshopify.com/admin/oauth/access_token",
scopes=["read_orders"],
scope_separator=",",
values={"shop": Ask("Your store name, the part before .myshopify.com")},
)Placeholders work in authorize_params too. Every placeholder must be filled, and every value must match a placeholder, so a typo fails as soon as the code is imported.
When a service does things its own way
Most services need only the URLs and scopes. For the rest, these options cover the usual differences:
| Option | Use it when |
|---|---|
authorize_params | The sign-in link needs extra values, such as {"audience": "api.example.com"} |
token_endpoint_auth_method="client_secret_basic" | The service wants the client ID and secret as HTTP Basic auth |
token_request_format="json" | The service wants the token request as JSON |
scope_separator="," | Scopes are separated by commas |
scope_param | Scopes go in another parameter |
pkce=True | The service requires PKCE |
token_path | The token sits deeper in the response, such as "authed_user.access_token" |
userinfo_url, account_field | Show people which account they connected |
revoke_url | Revoke the token when someone disconnects |
The connections docs have the full reference. Callables refreshes tokens, keeps rotated refresh tokens, and treats a token without an expiry as long-lived. For a service that only gives out API keys, use a secret instead.
Run your scripts on each person's own account
Declare a connection, push, and share. Everyone connects once and every run uses their account.