Connectors
Okta · Shopify · Salesforce · HubSpot · Your own

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:

OptionUse it when
authorize_paramsThe 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_paramScopes go in another parameter
pkce=TrueThe service requires PKCE
token_pathThe token sits deeper in the response, such as "authed_user.access_token"
userinfo_url, account_fieldShow people which account they connected
revoke_urlRevoke 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.

Read the connections docs

Other connectors